mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-23 05:56:28 +02:00
Issue #45: add lane parsing; refactor figure.
This commit is contained in:
parent
b63a0ed397
commit
735f19428d
5 changed files with 233 additions and 206 deletions
|
@ -11,6 +11,7 @@ from colour import Color
|
||||||
|
|
||||||
from roentgen import ui
|
from roentgen import ui
|
||||||
from roentgen.color import get_gradient_color
|
from roentgen.color import get_gradient_color
|
||||||
|
from roentgen.figure import Building, StyledFigure, Road
|
||||||
from roentgen.flinger import Flinger
|
from roentgen.flinger import Flinger
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
@ -21,200 +22,22 @@ from roentgen.osm_reader import (
|
||||||
Map, OSMMember, OSMNode, OSMRelation, OSMWay, Tagged
|
Map, OSMMember, OSMNode, OSMRelation, OSMWay, Tagged
|
||||||
)
|
)
|
||||||
from roentgen.point import Point
|
from roentgen.point import Point
|
||||||
from roentgen.scheme import DEFAULT_COLOR, LineStyle, RoadMatcher, Scheme
|
from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme
|
||||||
from roentgen.util import MinMax
|
from roentgen.util import MinMax
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
__author__ = "Sergey Vartanov"
|
__author__ = "Sergey Vartanov"
|
||||||
__email__ = "me@enzet.ru"
|
__email__ = "me@enzet.ru"
|
||||||
|
|
||||||
DEBUG: bool = False
|
DEBUG: bool = False
|
||||||
TIME_COLOR_SCALE: List[Color] = [
|
TIME_COLOR_SCALE: List[Color] = [
|
||||||
Color("#581845"), Color("#900C3F"), Color("#C70039"),
|
Color("#581845"),
|
||||||
Color("#FF5733"), Color("#FFC300"), Color("#DAF7A6"),
|
Color("#900C3F"),
|
||||||
|
Color("#C70039"),
|
||||||
|
Color("#FF5733"),
|
||||||
|
Color("#FFC300"),
|
||||||
|
Color("#DAF7A6"),
|
||||||
]
|
]
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
|
|
||||||
def is_clockwise(polygon: List[OSMNode]) -> bool:
|
|
||||||
"""
|
|
||||||
Return true if polygon nodes are in clockwise order.
|
|
||||||
|
|
||||||
:param polygon: list of OpenStreetMap nodes
|
|
||||||
"""
|
|
||||||
count: float = 0
|
|
||||||
for index, node in enumerate(polygon): # type: int, OSMNode
|
|
||||||
next_index: int = 0 if index == len(polygon) - 1 else index + 1
|
|
||||||
count += (polygon[next_index].coordinates[0] - node.coordinates[0]) * (
|
|
||||||
polygon[next_index].coordinates[1] + node.coordinates[1]
|
|
||||||
)
|
|
||||||
return count >= 0
|
|
||||||
|
|
||||||
|
|
||||||
def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
|
|
||||||
"""
|
|
||||||
Make polygon nodes clockwise.
|
|
||||||
|
|
||||||
:param polygon: list of OpenStreetMap nodes
|
|
||||||
"""
|
|
||||||
return polygon if is_clockwise(polygon) else list(reversed(polygon))
|
|
||||||
|
|
||||||
|
|
||||||
def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
|
|
||||||
"""
|
|
||||||
Make polygon nodes counter-clockwise.
|
|
||||||
|
|
||||||
:param polygon: list of OpenStreetMap nodes
|
|
||||||
"""
|
|
||||||
return polygon if not is_clockwise(polygon) else list(reversed(polygon))
|
|
||||||
|
|
||||||
|
|
||||||
class Figure(Tagged):
|
|
||||||
"""
|
|
||||||
Some figure on the map: way or area.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tags: Dict[str, str],
|
|
||||||
inners: List[List[OSMNode]],
|
|
||||||
outers: List[List[OSMNode]],
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.tags: Dict[str, str] = tags
|
|
||||||
self.inners: List[List[OSMNode]] = []
|
|
||||||
self.outers: List[List[OSMNode]] = []
|
|
||||||
|
|
||||||
for inner_nodes in inners:
|
|
||||||
self.inners.append(make_clockwise(inner_nodes))
|
|
||||||
for outer_nodes in outers:
|
|
||||||
self.outers.append(make_counter_clockwise(outer_nodes))
|
|
||||||
|
|
||||||
def get_path(
|
|
||||||
self, flinger: Flinger, shift: np.array = np.array((0, 0))
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Get SVG path commands.
|
|
||||||
|
|
||||||
:param flinger: converter for geo coordinates
|
|
||||||
:param shift: shift vector
|
|
||||||
"""
|
|
||||||
path: str = ""
|
|
||||||
|
|
||||||
for outer_nodes in self.outers:
|
|
||||||
path += f"{get_path(outer_nodes, shift, flinger)} "
|
|
||||||
|
|
||||||
for inner_nodes in self.inners:
|
|
||||||
path += f"{get_path(inner_nodes, shift, flinger)} "
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
class StyledFigure(Figure):
|
|
||||||
"""
|
|
||||||
Figure with stroke and fill style.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tags: Dict[str, str],
|
|
||||||
inners: List[List[OSMNode]],
|
|
||||||
outers: List[List[OSMNode]],
|
|
||||||
line_style: LineStyle,
|
|
||||||
):
|
|
||||||
super().__init__(tags, inners, outers)
|
|
||||||
self.line_style = line_style
|
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
|
||||||
"""
|
|
||||||
Line segment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, point_1: np.array, point_2: np.array):
|
|
||||||
self.point_1 = point_1
|
|
||||||
self.point_2 = point_2
|
|
||||||
|
|
||||||
difference: np.array = point_2 - point_1
|
|
||||||
vector: np.array = difference / np.linalg.norm(difference)
|
|
||||||
self.angle: float = np.arccos(np.dot(vector, np.array((0, 1)))) / np.pi
|
|
||||||
|
|
||||||
def __lt__(self, other: "Segment") -> bool:
|
|
||||||
return (
|
|
||||||
((self.point_1 + self.point_2) / 2)[1]
|
|
||||||
< ((other.point_1 + other.point_2) / 2)[1]
|
|
||||||
) # fmt: skip
|
|
||||||
|
|
||||||
|
|
||||||
class Building(Figure):
|
|
||||||
"""
|
|
||||||
Building on the map.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tags: Dict[str, str],
|
|
||||||
inners: List[List[OSMNode]],
|
|
||||||
outers: List[List[OSMNode]],
|
|
||||||
flinger: Flinger,
|
|
||||||
scheme: Scheme,
|
|
||||||
):
|
|
||||||
super().__init__(tags, inners, outers)
|
|
||||||
|
|
||||||
style: Dict[str, Any] = {
|
|
||||||
"fill": scheme.get_color("building_color").hex,
|
|
||||||
"stroke": scheme.get_color("building_border_color").hex,
|
|
||||||
}
|
|
||||||
self.line_style = LineStyle(style)
|
|
||||||
self.parts = []
|
|
||||||
|
|
||||||
for nodes in self.inners + self.outers:
|
|
||||||
for i in range(len(nodes) - 1):
|
|
||||||
flung_1: np.array = flinger.fling(nodes[i].coordinates)
|
|
||||||
flung_2: np.array = flinger.fling(nodes[i + 1].coordinates)
|
|
||||||
self.parts.append(Segment(flung_1, flung_2))
|
|
||||||
|
|
||||||
self.parts = sorted(self.parts)
|
|
||||||
|
|
||||||
def get_levels(self) -> float:
|
|
||||||
"""
|
|
||||||
Get building level number.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return max(3.0, float(self.get_tag("building:levels")))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return 3
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
):
|
|
||||||
super().__init__(tags, inners, outers)
|
|
||||||
self.matcher: RoadMatcher = matcher
|
|
||||||
|
|
||||||
self.width: Optional[float] = None
|
|
||||||
self.lanes: int = 1
|
|
||||||
|
|
||||||
if "lanes" in tags:
|
|
||||||
try:
|
|
||||||
self.width = float(tags["lanes"]) * 3.7
|
|
||||||
self.lanes = float(tags["lanes"])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if "width" in tags:
|
|
||||||
try:
|
|
||||||
self.width = float(tags["width"])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
|
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
|
||||||
|
@ -290,23 +113,6 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
|
|
||||||
"""
|
|
||||||
Construct SVG path commands from nodes.
|
|
||||||
"""
|
|
||||||
path: str = ""
|
|
||||||
prev_node: Optional[OSMNode] = None
|
|
||||||
for node in nodes: # type: OSMNode
|
|
||||||
flung = 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
|
|
||||||
|
|
||||||
|
|
||||||
def is_cycle(nodes) -> bool:
|
def is_cycle(nodes) -> bool:
|
||||||
"""
|
"""
|
||||||
Is way a cycle way or an area boundary.
|
Is way a cycle way or an area boundary.
|
||||||
|
|
215
roentgen/figure.py
Normal file
215
roentgen/figure.py
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from roentgen.flinger import Flinger
|
||||||
|
from roentgen.osm_reader import OSMNode, Tagged
|
||||||
|
from roentgen.road import Lane
|
||||||
|
from roentgen.scheme import Scheme, LineStyle, RoadMatcher
|
||||||
|
|
||||||
|
|
||||||
|
class Figure(Tagged):
|
||||||
|
"""
|
||||||
|
Some figure on the map: way or area.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
tags: Dict[str, str],
|
||||||
|
inners: List[List[OSMNode]],
|
||||||
|
outers: List[List[OSMNode]],
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.tags: Dict[str, str] = tags
|
||||||
|
self.inners: List[List[OSMNode]] = []
|
||||||
|
self.outers: List[List[OSMNode]] = []
|
||||||
|
|
||||||
|
for inner_nodes in inners:
|
||||||
|
self.inners.append(make_clockwise(inner_nodes))
|
||||||
|
for outer_nodes in outers:
|
||||||
|
self.outers.append(make_counter_clockwise(outer_nodes))
|
||||||
|
|
||||||
|
def get_path(
|
||||||
|
self, flinger: Flinger, shift: np.array = np.array((0, 0))
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Get SVG path commands.
|
||||||
|
|
||||||
|
:param flinger: converter for geo coordinates
|
||||||
|
:param shift: shift vector
|
||||||
|
"""
|
||||||
|
path: str = ""
|
||||||
|
|
||||||
|
for outer_nodes in self.outers:
|
||||||
|
path += f"{get_path(outer_nodes, shift, flinger)} "
|
||||||
|
|
||||||
|
for inner_nodes in self.inners:
|
||||||
|
path += f"{get_path(inner_nodes, shift, flinger)} "
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class Building(Figure):
|
||||||
|
"""
|
||||||
|
Building on the map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
tags: Dict[str, str],
|
||||||
|
inners: List[List[OSMNode]],
|
||||||
|
outers: List[List[OSMNode]],
|
||||||
|
flinger: Flinger,
|
||||||
|
scheme: Scheme,
|
||||||
|
):
|
||||||
|
super().__init__(tags, inners, outers)
|
||||||
|
|
||||||
|
style: Dict[str, Any] = {
|
||||||
|
"fill": scheme.get_color("building_color").hex,
|
||||||
|
"stroke": scheme.get_color("building_border_color").hex,
|
||||||
|
}
|
||||||
|
self.line_style = LineStyle(style)
|
||||||
|
self.parts = []
|
||||||
|
|
||||||
|
for nodes in self.inners + self.outers:
|
||||||
|
for i in range(len(nodes) - 1):
|
||||||
|
flung_1: np.array = flinger.fling(nodes[i].coordinates)
|
||||||
|
flung_2: np.array = flinger.fling(nodes[i + 1].coordinates)
|
||||||
|
self.parts.append(Segment(flung_1, flung_2))
|
||||||
|
|
||||||
|
self.parts = sorted(self.parts)
|
||||||
|
|
||||||
|
def get_levels(self) -> float:
|
||||||
|
"""
|
||||||
|
Get building level number.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return max(3.0, float(self.get_tag("building:levels")))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
|
class StyledFigure(Figure):
|
||||||
|
"""
|
||||||
|
Figure with stroke and fill style.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
tags: Dict[str, str],
|
||||||
|
inners: List[List[OSMNode]],
|
||||||
|
outers: List[List[OSMNode]],
|
||||||
|
line_style: LineStyle,
|
||||||
|
):
|
||||||
|
super().__init__(tags, inners, outers)
|
||||||
|
self.line_style = 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,
|
||||||
|
):
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Segment:
|
||||||
|
"""
|
||||||
|
Line segment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, point_1: np.array, point_2: np.array):
|
||||||
|
self.point_1 = point_1
|
||||||
|
self.point_2 = point_2
|
||||||
|
|
||||||
|
difference: np.array = point_2 - point_1
|
||||||
|
vector: np.array = difference / np.linalg.norm(difference)
|
||||||
|
self.angle: float = np.arccos(np.dot(vector, np.array((0, 1)))) / np.pi
|
||||||
|
|
||||||
|
def __lt__(self, other: "Segment") -> bool:
|
||||||
|
return (
|
||||||
|
((self.point_1 + self.point_2) / 2)[1]
|
||||||
|
< ((other.point_1 + other.point_2) / 2)[1]
|
||||||
|
) # fmt: skip
|
||||||
|
|
||||||
|
|
||||||
|
def is_clockwise(polygon: List[OSMNode]) -> bool:
|
||||||
|
"""
|
||||||
|
Return true if polygon nodes are in clockwise order.
|
||||||
|
|
||||||
|
:param polygon: list of OpenStreetMap nodes
|
||||||
|
"""
|
||||||
|
count: float = 0
|
||||||
|
for index, node in enumerate(polygon): # type: int, OSMNode
|
||||||
|
next_index: int = 0 if index == len(polygon) - 1 else index + 1
|
||||||
|
count += (polygon[next_index].coordinates[0] - node.coordinates[0]) * (
|
||||||
|
polygon[next_index].coordinates[1] + node.coordinates[1]
|
||||||
|
)
|
||||||
|
return count >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
|
||||||
|
"""
|
||||||
|
Make polygon nodes clockwise.
|
||||||
|
|
||||||
|
:param polygon: list of OpenStreetMap nodes
|
||||||
|
"""
|
||||||
|
return polygon if is_clockwise(polygon) else list(reversed(polygon))
|
||||||
|
|
||||||
|
|
||||||
|
def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
|
||||||
|
"""
|
||||||
|
Make polygon nodes counter-clockwise.
|
||||||
|
|
||||||
|
:param polygon: list of OpenStreetMap nodes
|
||||||
|
"""
|
||||||
|
return polygon if not is_clockwise(polygon) else list(reversed(polygon))
|
||||||
|
|
||||||
|
|
||||||
|
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
|
||||||
|
"""
|
||||||
|
Construct SVG path commands from nodes.
|
||||||
|
"""
|
||||||
|
path: str = ""
|
||||||
|
prev_node: Optional[OSMNode] = None
|
||||||
|
for node in nodes: # type: OSMNode
|
||||||
|
flung = 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
|
|
@ -11,8 +11,9 @@ from svgwrite.path import Path
|
||||||
from svgwrite.shapes import Rect
|
from svgwrite.shapes import Rect
|
||||||
|
|
||||||
from roentgen import ui
|
from roentgen import ui
|
||||||
from roentgen.constructor import Building, Constructor, Road, Segment
|
from roentgen.constructor import Constructor
|
||||||
from roentgen.direction import DirectionSet, Sector
|
from roentgen.direction import DirectionSet, Sector
|
||||||
|
from roentgen.figure import Building, Road, Segment
|
||||||
from roentgen.flinger import Flinger
|
from roentgen.flinger import Flinger
|
||||||
from roentgen.icon import ShapeExtractor
|
from roentgen.icon import ShapeExtractor
|
||||||
from roentgen.osm_reader import Map
|
from roentgen.osm_reader import Map
|
||||||
|
|
|
@ -7,7 +7,6 @@ from typing import List, Optional
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import svgwrite
|
import svgwrite
|
||||||
|
|
||||||
from roentgen.constructor import Road
|
|
||||||
from roentgen.flinger import Flinger
|
from roentgen.flinger import Flinger
|
||||||
from roentgen.vector import angle, turn_by_angle, norm, Line
|
from roentgen.vector import angle, turn_by_angle, norm, Line
|
||||||
from roentgen.osm_reader import OSMNode
|
from roentgen.osm_reader import OSMNode
|
||||||
|
@ -27,6 +26,9 @@ class Lane:
|
||||||
change: Optional[str] = None # "not_left", "not_right"
|
change: Optional[str] = None # "not_left", "not_right"
|
||||||
destination: Optional[str] = None # Lane destination
|
destination: Optional[str] = None # Lane destination
|
||||||
|
|
||||||
|
def set_forward(self, is_forward: bool) -> None:
|
||||||
|
self.is_forward = is_forward
|
||||||
|
|
||||||
|
|
||||||
class RoadPart:
|
class RoadPart:
|
||||||
"""
|
"""
|
||||||
|
@ -71,7 +73,7 @@ class RoadPart:
|
||||||
node_1: OSMNode,
|
node_1: OSMNode,
|
||||||
node_2: OSMNode,
|
node_2: OSMNode,
|
||||||
flinger: Flinger,
|
flinger: Flinger,
|
||||||
road: Road,
|
road,
|
||||||
) -> "RoadPart":
|
) -> "RoadPart":
|
||||||
"""
|
"""
|
||||||
Construct road part from OSM nodes.
|
Construct road part from OSM nodes.
|
||||||
|
|
|
@ -12,6 +12,9 @@ __email__ = "me@enzet.ru"
|
||||||
BOXES: List[str] = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
|
BOXES: List[str] = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
|
||||||
BOXES_LENGTH: int = len(BOXES)
|
BOXES_LENGTH: int = len(BOXES)
|
||||||
|
|
||||||
|
AUTHOR_MODE: str = "author"
|
||||||
|
TIME_MODE: str = "time"
|
||||||
|
|
||||||
|
|
||||||
def parse_options(args) -> argparse.Namespace:
|
def parse_options(args) -> argparse.Namespace:
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue