Issue #45: add lane parsing; refactor figure.

This commit is contained in:
Sergey Vartanov 2021-06-03 04:39:05 +03:00
parent b63a0ed397
commit 735f19428d
5 changed files with 233 additions and 206 deletions

View file

@ -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
View 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

View file

@ -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

View file

@ -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.

View file

@ -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:
""" """