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,
|
||||
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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]],
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue