Issue #84: add primitive road connections.

This commit is contained in:
Sergey Vartanov 2021-09-18 20:25:51 +03:00
parent b46e65f2ec
commit aa82353f00
6 changed files with 258 additions and 150 deletions

View file

@ -15,10 +15,10 @@ from map_machine.figure import (
Building,
Crater,
DirectionSector,
Road,
StyledFigure,
Tree,
)
from map_machine.road import Road, Roads
from map_machine.flinger import Flinger
from map_machine.icon import (
DEFAULT_SMALL_SHAPE_ID,
@ -185,7 +185,7 @@ class Constructor:
self.points: list[Point] = []
self.figures: list[StyledFigure] = []
self.buildings: list[Building] = []
self.roads: list[Road] = []
self.roads: Roads = Roads()
self.trees: list[Tree] = []
self.craters: list[Crater] = []
self.direction_sectors: list[DirectionSector] = []
@ -260,7 +260,9 @@ class Constructor:
road_matcher: RoadMatcher = self.scheme.get_road(line.tags)
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
processed: set[str] = set()

View file

@ -5,7 +5,6 @@ from typing import Any, Iterator, Optional
import numpy as np
from colour import Color
from shapely.geometry import LineString
from svgwrite import Drawing
from svgwrite.container import Group
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.flinger import Flinger
from map_machine.osm_reader import OSMNode, Tagged
from map_machine.road import Lane
from map_machine.scheme import LineStyle, RoadMatcher, Scheme
from map_machine.scheme import LineStyle, Scheme
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
from map_machine.vector import Polyline
BUILDING_HEIGHT_SCALE: float = 2.5
BUILDING_MINIMAL_HEIGHT: float = 8.0
@ -61,20 +61,6 @@ class Figure(Tagged):
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):
"""
@ -221,101 +207,6 @@ class StyledFigure(Figure):
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):
"""
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:
"""Construct SVG path commands from nodes."""
path: str = ""
prev_node: Optional[OSMNode] = None
for node in nodes:
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
return Polyline(
[flinger.fling(x.coordinates) + shift for x in nodes]
).get_path()

View file

@ -15,14 +15,14 @@ from svgwrite.shapes import Rect
from map_machine.boundary_box import BoundaryBox
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.icon import ShapeExtractor
from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm_getter import NetworkError, get_osm
from map_machine.osm_reader import OSMData, OSMNode
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.ui import BuildingMode, progress_bar
from map_machine.workspace import workspace
@ -70,24 +70,7 @@ class Map:
self.svg.add(path)
progress_bar(-1, 0, text="Drawing ways")
layered_roads: dict[float, list[Road]] = {}
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
)
constructor.roads.draw(self.svg, self.flinger)
for tree in constructor.trees:
tree.draw(self.svg, self.flinger, self.scheme)
@ -218,6 +201,8 @@ def ui(arguments: argparse.Namespace) -> None:
if 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:
if arguments.boundary_box:
boundary_box = BoundaryBox.from_text(arguments.boundary_box)

View file

@ -151,8 +151,8 @@ class OSMNode(Tagged):
:param structure: input structure
"""
return cls(
structure["id"],
structure["tags"] if "tags" in structure else {},
structure["id"],
coordinates=np.array((structure["lat"], structure["lon"])),
)
@ -209,7 +209,7 @@ class OSMWay(Tagged):
:param nodes: node structure
"""
return cls(
structure["tags"],
structure["tags"] if "tags" in structure else {},
structure["id"],
[nodes[x] for x in structure["nodes"]],
)

View file

@ -2,13 +2,26 @@
WIP: road shape drawing.
"""
from dataclasses import dataclass
from typing import Optional
from typing import Any, Optional
import numpy as np
import svgwrite
from colour import Color
from svgwrite import Drawing
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"
__email__ = "me@enzet.ru"
@ -345,3 +358,195 @@ class Intersection:
# for part in self.parts:
# part.draw_lanes(drawing, scale)
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)

View file

@ -6,6 +6,8 @@ import numpy as np
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
from shapely.geometry import LineString
def compute_angle(vector: np.ndarray) -> float:
"""
@ -38,6 +40,37 @@ def norm(vector: np.ndarray) -> np.ndarray:
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:
"""Infinity line: Ax + By + C = 0."""