mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-22 21:46:24 +02:00
Issue #84: add primitive road connections.
This commit is contained in:
parent
b46e65f2ec
commit
aa82353f00
6 changed files with 258 additions and 150 deletions
|
@ -15,10 +15,10 @@ from map_machine.figure import (
|
||||||
Building,
|
Building,
|
||||||
Crater,
|
Crater,
|
||||||
DirectionSector,
|
DirectionSector,
|
||||||
Road,
|
|
||||||
StyledFigure,
|
StyledFigure,
|
||||||
Tree,
|
Tree,
|
||||||
)
|
)
|
||||||
|
from map_machine.road import Road, Roads
|
||||||
from map_machine.flinger import Flinger
|
from map_machine.flinger import Flinger
|
||||||
from map_machine.icon import (
|
from map_machine.icon import (
|
||||||
DEFAULT_SMALL_SHAPE_ID,
|
DEFAULT_SMALL_SHAPE_ID,
|
||||||
|
@ -185,7 +185,7 @@ class Constructor:
|
||||||
self.points: list[Point] = []
|
self.points: list[Point] = []
|
||||||
self.figures: list[StyledFigure] = []
|
self.figures: list[StyledFigure] = []
|
||||||
self.buildings: list[Building] = []
|
self.buildings: list[Building] = []
|
||||||
self.roads: list[Road] = []
|
self.roads: Roads = Roads()
|
||||||
self.trees: list[Tree] = []
|
self.trees: list[Tree] = []
|
||||||
self.craters: list[Crater] = []
|
self.craters: list[Crater] = []
|
||||||
self.direction_sectors: list[DirectionSector] = []
|
self.direction_sectors: list[DirectionSector] = []
|
||||||
|
@ -260,7 +260,9 @@ class Constructor:
|
||||||
|
|
||||||
road_matcher: RoadMatcher = self.scheme.get_road(line.tags)
|
road_matcher: RoadMatcher = self.scheme.get_road(line.tags)
|
||||||
if road_matcher:
|
if road_matcher:
|
||||||
self.roads.append(Road(line.tags, inners, outers, road_matcher))
|
self.roads.append(
|
||||||
|
Road(line.tags, outers[0], road_matcher, self.flinger)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
processed: set[str] = set()
|
processed: set[str] = set()
|
||||||
|
|
|
@ -5,7 +5,6 @@ from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from colour import Color
|
from colour import Color
|
||||||
from shapely.geometry import LineString
|
|
||||||
from svgwrite import Drawing
|
from svgwrite import Drawing
|
||||||
from svgwrite.container import Group
|
from svgwrite.container import Group
|
||||||
from svgwrite.path import Path
|
from svgwrite.path import Path
|
||||||
|
@ -14,12 +13,13 @@ from map_machine.direction import DirectionSet, Sector
|
||||||
from map_machine.drawing import PathCommands
|
from map_machine.drawing import PathCommands
|
||||||
from map_machine.flinger import Flinger
|
from map_machine.flinger import Flinger
|
||||||
from map_machine.osm_reader import OSMNode, Tagged
|
from map_machine.osm_reader import OSMNode, Tagged
|
||||||
from map_machine.road import Lane
|
from map_machine.scheme import LineStyle, Scheme
|
||||||
from map_machine.scheme import LineStyle, RoadMatcher, Scheme
|
|
||||||
|
|
||||||
__author__ = "Sergey Vartanov"
|
__author__ = "Sergey Vartanov"
|
||||||
__email__ = "me@enzet.ru"
|
__email__ = "me@enzet.ru"
|
||||||
|
|
||||||
|
from map_machine.vector import Polyline
|
||||||
|
|
||||||
BUILDING_HEIGHT_SCALE: float = 2.5
|
BUILDING_HEIGHT_SCALE: float = 2.5
|
||||||
BUILDING_MINIMAL_HEIGHT: float = 8.0
|
BUILDING_MINIMAL_HEIGHT: float = 8.0
|
||||||
|
|
||||||
|
@ -61,20 +61,6 @@ class Figure(Tagged):
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def get_outer_path(
|
|
||||||
self, flinger: Flinger, parallel_offset: float = 0
|
|
||||||
) -> str:
|
|
||||||
"""Get path of the first outer node list."""
|
|
||||||
points: list[tuple[float, float]] = [
|
|
||||||
tuple(flinger.fling(x.coordinates)) for x in self.outers[0]
|
|
||||||
]
|
|
||||||
offset = LineString(points).parallel_offset(parallel_offset)
|
|
||||||
|
|
||||||
path: str = ""
|
|
||||||
for index, point in enumerate(offset.coords):
|
|
||||||
path += ("L" if index else "M") + f" {point[0]},{point[1]} "
|
|
||||||
return path[:-1]
|
|
||||||
|
|
||||||
|
|
||||||
class Building(Figure):
|
class Building(Figure):
|
||||||
"""
|
"""
|
||||||
|
@ -221,101 +207,6 @@ class StyledFigure(Figure):
|
||||||
self.line_style: LineStyle = line_style
|
self.line_style: LineStyle = line_style
|
||||||
|
|
||||||
|
|
||||||
class Road(Figure):
|
|
||||||
"""
|
|
||||||
Road or track on the map.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tags: dict[str, str],
|
|
||||||
inners: list[list[OSMNode]],
|
|
||||||
outers: list[list[OSMNode]],
|
|
||||||
matcher: RoadMatcher,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(tags, inners, outers)
|
|
||||||
self.matcher: RoadMatcher = matcher
|
|
||||||
|
|
||||||
self.width: Optional[float] = None
|
|
||||||
self.lanes: list[Lane] = []
|
|
||||||
|
|
||||||
if "lanes" in tags:
|
|
||||||
try:
|
|
||||||
self.width = int(tags["lanes"]) * 3.7
|
|
||||||
self.lanes = [Lane()] * int(tags["lanes"])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
number: int
|
|
||||||
if "lanes:forward" in tags:
|
|
||||||
number = int(tags["lanes:forward"])
|
|
||||||
[x.set_forward(True) for x in self.lanes[-number:]]
|
|
||||||
if "lanes:backward" in tags:
|
|
||||||
number = int(tags["lanes:backward"])
|
|
||||||
[x.set_forward(False) for x in self.lanes[:number]]
|
|
||||||
|
|
||||||
if "width" in tags:
|
|
||||||
try:
|
|
||||||
self.width = float(tags["width"])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.layer: float = 0
|
|
||||||
if "layer" in tags:
|
|
||||||
self.layer = float(tags["layer"])
|
|
||||||
|
|
||||||
def draw(
|
|
||||||
self,
|
|
||||||
svg: Drawing,
|
|
||||||
flinger: Flinger,
|
|
||||||
color: Color,
|
|
||||||
extra_width: float = 0,
|
|
||||||
) -> None:
|
|
||||||
"""Draw road as simple SVG path."""
|
|
||||||
flinger.get_scale()
|
|
||||||
width: float
|
|
||||||
if self.width is not None:
|
|
||||||
width = self.width
|
|
||||||
else:
|
|
||||||
width = self.matcher.default_width
|
|
||||||
cap: str = "round"
|
|
||||||
if extra_width:
|
|
||||||
cap = "butt"
|
|
||||||
if self.tags.get("bridge") == "yes":
|
|
||||||
color = Color("#666666")
|
|
||||||
scale: float = flinger.get_scale(self.outers[0][0].coordinates)
|
|
||||||
path_commands: str = self.get_path(flinger)
|
|
||||||
path: Path = Path(d=path_commands)
|
|
||||||
style: dict[str, Any] = {
|
|
||||||
"fill": "none",
|
|
||||||
"stroke": color.hex,
|
|
||||||
"stroke-linecap": cap,
|
|
||||||
"stroke-linejoin": "round",
|
|
||||||
"stroke-width": scale * width + extra_width,
|
|
||||||
}
|
|
||||||
path.update(style)
|
|
||||||
svg.add(path)
|
|
||||||
|
|
||||||
def draw_lanes(self, svg: Drawing, flinger: Flinger, color: Color) -> None:
|
|
||||||
scale: float = flinger.get_scale(self.outers[0][0].coordinates)
|
|
||||||
if len(self.lanes) < 2:
|
|
||||||
return
|
|
||||||
for index in range(1, len(self.lanes)):
|
|
||||||
shift = scale * (
|
|
||||||
-self.width / 2 + index * self.width / len(self.lanes)
|
|
||||||
)
|
|
||||||
path: Path = Path(d=self.get_outer_path(flinger, shift))
|
|
||||||
style: dict[str, Any] = {
|
|
||||||
"fill": "none",
|
|
||||||
"stroke": color.hex,
|
|
||||||
"stroke-linejoin": "round",
|
|
||||||
"stroke-width": 1,
|
|
||||||
"opacity": 0.5,
|
|
||||||
}
|
|
||||||
path.update(style)
|
|
||||||
svg.add(path)
|
|
||||||
|
|
||||||
|
|
||||||
class Crater(Tagged):
|
class Crater(Tagged):
|
||||||
"""
|
"""
|
||||||
Volcano or impact crater on the map.
|
Volcano or impact crater on the map.
|
||||||
|
@ -510,14 +401,6 @@ def make_counter_clockwise(polygon: list[OSMNode]) -> list[OSMNode]:
|
||||||
|
|
||||||
def get_path(nodes: list[OSMNode], shift: np.ndarray, flinger: Flinger) -> str:
|
def get_path(nodes: list[OSMNode], shift: np.ndarray, flinger: Flinger) -> str:
|
||||||
"""Construct SVG path commands from nodes."""
|
"""Construct SVG path commands from nodes."""
|
||||||
path: str = ""
|
return Polyline(
|
||||||
prev_node: Optional[OSMNode] = None
|
[flinger.fling(x.coordinates) + shift for x in nodes]
|
||||||
for node in nodes:
|
).get_path()
|
||||||
flung: np.ndarray = flinger.fling(node.coordinates) + shift
|
|
||||||
path += ("L" if prev_node else "M") + f" {flung[0]},{flung[1]} "
|
|
||||||
prev_node = node
|
|
||||||
if nodes[0] == nodes[-1]:
|
|
||||||
path += "Z"
|
|
||||||
else:
|
|
||||||
path = path[:-1]
|
|
||||||
return path
|
|
||||||
|
|
|
@ -15,14 +15,14 @@ from svgwrite.shapes import Rect
|
||||||
|
|
||||||
from map_machine.boundary_box import BoundaryBox
|
from map_machine.boundary_box import BoundaryBox
|
||||||
from map_machine.constructor import Constructor
|
from map_machine.constructor import Constructor
|
||||||
from map_machine.figure import Road, StyledFigure
|
from map_machine.figure import StyledFigure
|
||||||
from map_machine.flinger import Flinger
|
from map_machine.flinger import Flinger
|
||||||
from map_machine.icon import ShapeExtractor
|
from map_machine.icon import ShapeExtractor
|
||||||
from map_machine.map_configuration import LabelMode, MapConfiguration
|
from map_machine.map_configuration import LabelMode, MapConfiguration
|
||||||
from map_machine.osm_getter import NetworkError, get_osm
|
from map_machine.osm_getter import NetworkError, get_osm
|
||||||
from map_machine.osm_reader import OSMData, OSMNode
|
from map_machine.osm_reader import OSMData, OSMNode
|
||||||
from map_machine.point import Occupied, Point
|
from map_machine.point import Occupied, Point
|
||||||
from map_machine.road import Intersection, RoadPart
|
from map_machine.road import Intersection, Road, RoadPart
|
||||||
from map_machine.scheme import Scheme
|
from map_machine.scheme import Scheme
|
||||||
from map_machine.ui import BuildingMode, progress_bar
|
from map_machine.ui import BuildingMode, progress_bar
|
||||||
from map_machine.workspace import workspace
|
from map_machine.workspace import workspace
|
||||||
|
@ -70,24 +70,7 @@ class Map:
|
||||||
self.svg.add(path)
|
self.svg.add(path)
|
||||||
progress_bar(-1, 0, text="Drawing ways")
|
progress_bar(-1, 0, text="Drawing ways")
|
||||||
|
|
||||||
layered_roads: dict[float, list[Road]] = {}
|
constructor.roads.draw(self.svg, self.flinger)
|
||||||
for road in constructor.roads:
|
|
||||||
if road.layer not in layered_roads:
|
|
||||||
layered_roads[road.layer] = []
|
|
||||||
layered_roads[road.layer].append(road)
|
|
||||||
|
|
||||||
for layer in sorted(layered_roads.keys()):
|
|
||||||
roads = sorted(
|
|
||||||
layered_roads[layer], key=lambda x: x.matcher.priority
|
|
||||||
)
|
|
||||||
for road in roads:
|
|
||||||
road.draw(self.svg, self.flinger, road.matcher.border_color, 2)
|
|
||||||
for road in roads:
|
|
||||||
road.draw(self.svg, self.flinger, road.matcher.color)
|
|
||||||
for road in roads:
|
|
||||||
road.draw_lanes(
|
|
||||||
self.svg, self.flinger, road.matcher.border_color
|
|
||||||
)
|
|
||||||
|
|
||||||
for tree in constructor.trees:
|
for tree in constructor.trees:
|
||||||
tree.draw(self.svg, self.flinger, self.scheme)
|
tree.draw(self.svg, self.flinger, self.scheme)
|
||||||
|
@ -218,6 +201,8 @@ def ui(arguments: argparse.Namespace) -> None:
|
||||||
|
|
||||||
if arguments.input_file_names:
|
if arguments.input_file_names:
|
||||||
input_file_names = list(map(Path, arguments.input_file_names))
|
input_file_names = list(map(Path, arguments.input_file_names))
|
||||||
|
if arguments.boundary_box:
|
||||||
|
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
|
||||||
else:
|
else:
|
||||||
if arguments.boundary_box:
|
if arguments.boundary_box:
|
||||||
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
|
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
|
||||||
|
|
|
@ -151,8 +151,8 @@ class OSMNode(Tagged):
|
||||||
:param structure: input structure
|
:param structure: input structure
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
structure["id"],
|
|
||||||
structure["tags"] if "tags" in structure else {},
|
structure["tags"] if "tags" in structure else {},
|
||||||
|
structure["id"],
|
||||||
coordinates=np.array((structure["lat"], structure["lon"])),
|
coordinates=np.array((structure["lat"], structure["lon"])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -209,7 +209,7 @@ class OSMWay(Tagged):
|
||||||
:param nodes: node structure
|
:param nodes: node structure
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
structure["tags"],
|
structure["tags"] if "tags" in structure else {},
|
||||||
structure["id"],
|
structure["id"],
|
||||||
[nodes[x] for x in structure["nodes"]],
|
[nodes[x] for x in structure["nodes"]],
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,13 +2,26 @@
|
||||||
WIP: road shape drawing.
|
WIP: road shape drawing.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import svgwrite
|
import svgwrite
|
||||||
|
from colour import Color
|
||||||
|
from svgwrite import Drawing
|
||||||
from svgwrite.path import Path
|
from svgwrite.path import Path
|
||||||
|
|
||||||
from map_machine.vector import Line, compute_angle, norm, turn_by_angle
|
from map_machine.drawing import PathCommands
|
||||||
|
from map_machine.flinger import Flinger
|
||||||
|
from map_machine.osm_reader import OSMNode, Tagged
|
||||||
|
from map_machine.scheme import RoadMatcher
|
||||||
|
|
||||||
|
from map_machine.vector import (
|
||||||
|
Line,
|
||||||
|
Polyline,
|
||||||
|
compute_angle,
|
||||||
|
norm,
|
||||||
|
turn_by_angle,
|
||||||
|
)
|
||||||
|
|
||||||
__author__ = "Sergey Vartanov"
|
__author__ = "Sergey Vartanov"
|
||||||
__email__ = "me@enzet.ru"
|
__email__ = "me@enzet.ru"
|
||||||
|
@ -345,3 +358,195 @@ class Intersection:
|
||||||
# for part in self.parts:
|
# for part in self.parts:
|
||||||
# part.draw_lanes(drawing, scale)
|
# part.draw_lanes(drawing, scale)
|
||||||
drawing.add(drawing.path(inner_commands, fill="#FF8888"))
|
drawing.add(drawing.path(inner_commands, fill="#FF8888"))
|
||||||
|
|
||||||
|
|
||||||
|
class Road(Tagged):
|
||||||
|
"""
|
||||||
|
Road or track on the map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
tags: dict[str, str],
|
||||||
|
nodes: list[OSMNode],
|
||||||
|
matcher: RoadMatcher,
|
||||||
|
flinger: Flinger,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(tags)
|
||||||
|
self.nodes: list[OSMNode] = nodes
|
||||||
|
self.matcher: RoadMatcher = matcher
|
||||||
|
|
||||||
|
self.line: Polyline = Polyline(
|
||||||
|
[flinger.fling(x.coordinates) for x in self.nodes]
|
||||||
|
)
|
||||||
|
self.width: Optional[float] = 5
|
||||||
|
self.lanes: list[Lane] = []
|
||||||
|
|
||||||
|
if "lanes" in tags:
|
||||||
|
try:
|
||||||
|
self.width = int(tags["lanes"]) * 3.7
|
||||||
|
self.lanes = [Lane()] * int(tags["lanes"])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
number: int
|
||||||
|
if "lanes:forward" in tags:
|
||||||
|
number = int(tags["lanes:forward"])
|
||||||
|
[x.set_forward(True) for x in self.lanes[-number:]]
|
||||||
|
if "lanes:backward" in tags:
|
||||||
|
number = int(tags["lanes:backward"])
|
||||||
|
[x.set_forward(False) for x in self.lanes[:number]]
|
||||||
|
|
||||||
|
if "width" in tags:
|
||||||
|
try:
|
||||||
|
self.width = float(tags["width"])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.layer: float = 0
|
||||||
|
if "layer" in tags:
|
||||||
|
self.layer = float(tags["layer"])
|
||||||
|
|
||||||
|
def draw(
|
||||||
|
self,
|
||||||
|
svg: Drawing,
|
||||||
|
flinger: Flinger,
|
||||||
|
color: Color,
|
||||||
|
extra_width: float = 0,
|
||||||
|
) -> None:
|
||||||
|
"""Draw road as simple SVG path."""
|
||||||
|
width: float
|
||||||
|
if self.width is not None:
|
||||||
|
width = self.width
|
||||||
|
else:
|
||||||
|
width = self.matcher.default_width
|
||||||
|
if extra_width and self.tags.get("bridge") == "yes":
|
||||||
|
color = Color("#666666")
|
||||||
|
scale: float = flinger.get_scale(self.nodes[0].coordinates)
|
||||||
|
path_commands: str = self.line.get_path()
|
||||||
|
path: Path = Path(d=path_commands)
|
||||||
|
style: dict[str, Any] = {
|
||||||
|
"fill": "none",
|
||||||
|
"stroke": color.hex,
|
||||||
|
"stroke-linecap": "butt",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
"stroke-width": scale * width + extra_width,
|
||||||
|
}
|
||||||
|
path.update(style)
|
||||||
|
svg.add(path)
|
||||||
|
|
||||||
|
def draw_lanes(self, svg: Drawing, flinger: Flinger, 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 * (
|
||||||
|
-self.width / 2 + index * self.width / len(self.lanes)
|
||||||
|
)
|
||||||
|
path: Path = Path(d=self.line.get_path(parallel_offset))
|
||||||
|
style: dict[str, Any] = {
|
||||||
|
"fill": "none",
|
||||||
|
"stroke": color.hex,
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
"stroke-width": 1,
|
||||||
|
"opacity": 0.5,
|
||||||
|
}
|
||||||
|
path.update(style)
|
||||||
|
svg.add(path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_curve_points(
|
||||||
|
road: Road, scale: float, center: np.ndarray, road_end: np.ndarray
|
||||||
|
) -> list[np.ndarray]:
|
||||||
|
"""
|
||||||
|
:param road: road segment
|
||||||
|
:param scale: current zoom scale
|
||||||
|
:param center: road intersection point
|
||||||
|
:param road_end: end point of the road segment
|
||||||
|
"""
|
||||||
|
width: float = road.width / 2.0 * scale + 0.5
|
||||||
|
|
||||||
|
direction: np.ndarray = (road_end - center) / np.linalg.norm(
|
||||||
|
road_end - center
|
||||||
|
)
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
class Roads:
|
||||||
|
"""
|
||||||
|
Whole road structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.roads: list[Road] = []
|
||||||
|
self.connections: dict[int, list[tuple[Road, int]]] = {}
|
||||||
|
|
||||||
|
def append(self, road: Road) -> None:
|
||||||
|
"""Add road and update connections."""
|
||||||
|
self.roads.append(road)
|
||||||
|
for index in road.nodes[0].id_, road.nodes[-1].id_:
|
||||||
|
if index not in self.connections:
|
||||||
|
self.connections[index] = []
|
||||||
|
self.connections[road.nodes[0].id_].append((road, 0))
|
||||||
|
self.connections[road.nodes[-1].id_].append((road, -1))
|
||||||
|
|
||||||
|
def draw(self, svg: Drawing, flinger: Flinger) -> None:
|
||||||
|
"""Draw whole road system."""
|
||||||
|
scale: float = flinger.get_scale(self.roads[0].nodes[0].coordinates)
|
||||||
|
layered_roads: dict[float, list[Road]] = {}
|
||||||
|
for road in self.roads:
|
||||||
|
if road.layer not in layered_roads:
|
||||||
|
layered_roads[road.layer] = []
|
||||||
|
layered_roads[road.layer].append(road)
|
||||||
|
for index in 0, -1:
|
||||||
|
id_: int = road.nodes[index].id_
|
||||||
|
if len(self.connections[id_]) != 2:
|
||||||
|
continue
|
||||||
|
road.line.shorten(index)
|
||||||
|
|
||||||
|
for layer in sorted(layered_roads.keys()):
|
||||||
|
roads = sorted(
|
||||||
|
layered_roads[layer], key=lambda x: x.matcher.priority
|
||||||
|
)
|
||||||
|
for road in roads:
|
||||||
|
road.draw(svg, flinger, road.matcher.border_color, 2)
|
||||||
|
for road in roads:
|
||||||
|
road.draw(svg, flinger, road.matcher.color)
|
||||||
|
for id_ in self.connections:
|
||||||
|
if len(self.connections[id_]) != 2:
|
||||||
|
continue
|
||||||
|
connected: list[tuple[Road, int]] = self.connections[id_]
|
||||||
|
road_1, index_1 = connected[0]
|
||||||
|
road_2, index_2 = connected[1]
|
||||||
|
node: OSMNode = road_1.nodes[index_1]
|
||||||
|
point = flinger.fling(node.coordinates)
|
||||||
|
|
||||||
|
c1: PathCommands = get_curve_points(
|
||||||
|
road_1, scale, point, road_1.line.points[index_1]
|
||||||
|
)
|
||||||
|
c2: PathCommands = get_curve_points(
|
||||||
|
road_2, scale, point, road_2.line.points[index_2]
|
||||||
|
)
|
||||||
|
curve_1 = [c1[0], "C", c1[1], c2[2], c2[3]]
|
||||||
|
curve_2 = [c2[0], "C", c2[1], c1[2], c1[3]]
|
||||||
|
|
||||||
|
path = svg.path(
|
||||||
|
d=["M"] + curve_1 + ["L"] + curve_2 + ["Z"],
|
||||||
|
fill=road_1.matcher.color.hex,
|
||||||
|
)
|
||||||
|
svg.add(path)
|
||||||
|
|
||||||
|
for curve in curve_1, curve_2:
|
||||||
|
path = svg.path(
|
||||||
|
d=["M"] + curve,
|
||||||
|
fill="none",
|
||||||
|
stroke=road_1.matcher.border_color.hex,
|
||||||
|
)
|
||||||
|
svg.add(path)
|
||||||
|
|
||||||
|
for road in roads:
|
||||||
|
road.draw_lanes(svg, flinger, road.matcher.border_color)
|
||||||
|
|
|
@ -6,6 +6,8 @@ import numpy as np
|
||||||
__author__ = "Sergey Vartanov"
|
__author__ = "Sergey Vartanov"
|
||||||
__email__ = "me@enzet.ru"
|
__email__ = "me@enzet.ru"
|
||||||
|
|
||||||
|
from shapely.geometry import LineString
|
||||||
|
|
||||||
|
|
||||||
def compute_angle(vector: np.ndarray) -> float:
|
def compute_angle(vector: np.ndarray) -> float:
|
||||||
"""
|
"""
|
||||||
|
@ -38,6 +40,37 @@ def norm(vector: np.ndarray) -> np.ndarray:
|
||||||
return vector / np.linalg.norm(vector)
|
return vector / np.linalg.norm(vector)
|
||||||
|
|
||||||
|
|
||||||
|
class Polyline:
|
||||||
|
"""
|
||||||
|
List of connected points.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, points: list[np.ndarray]) -> None:
|
||||||
|
self.points: list[np.ndarray] = points
|
||||||
|
|
||||||
|
def get_path(self, parallel_offset: float = 0) -> str:
|
||||||
|
"""Construct SVG path commands."""
|
||||||
|
points: list[np.ndarray]
|
||||||
|
try:
|
||||||
|
points = (
|
||||||
|
LineString(self.points).parallel_offset(parallel_offset).coords
|
||||||
|
if parallel_offset
|
||||||
|
else self.points
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
points = self.points
|
||||||
|
path: str = "M " + " L ".join(f"{x[0]},{x[1]}" for x in points)
|
||||||
|
return path + (" Z" if np.allclose(points[0], points[-1]) else "")
|
||||||
|
|
||||||
|
def shorten(self, index: int) -> None:
|
||||||
|
"""Make shorten part specified with index."""
|
||||||
|
index_2: int = 1 if index == 0 else -2
|
||||||
|
diff: np.ndarray = self.points[index_2] - self.points[index]
|
||||||
|
self.points[index] = (
|
||||||
|
self.points[index] + diff / np.linalg.norm(diff) * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Line:
|
class Line:
|
||||||
"""Infinity line: Ax + By + C = 0."""
|
"""Infinity line: Ax + By + C = 0."""
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue