map-machine/map_machine/feature/direction.py
2021-11-18 07:54:19 +03:00

254 lines
7.9 KiB
Python

"""
Direction tag support.
"""
from typing import Iterator, Optional
import numpy as np
from colour import Color
from portolan import middle
from svgwrite import Drawing
from svgwrite.gradients import RadialGradient
from svgwrite.path import Path
from map_machine.drawing import PathCommands
from map_machine.osm.osm_reader import Tagged
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
SHIFT: float = -np.pi / 2.0
SMALLEST_ANGLE: float = np.pi / 15.0
DEFAULT_ANGLE: float = np.pi / 30.0
def parse_vector(text: str) -> Optional[np.ndarray]:
"""
Parse vector from text representation: compass points or 360-degree
notation. E.g. "NW", "270".
:param text: vector text representation
:return: parsed normalized vector
"""
try:
radians: float = np.radians(float(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except ValueError:
pass
try:
radians: float = np.radians(middle(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except KeyError:
pass
return None
def rotation_matrix(angle: float) -> np.ndarray:
"""
Get a matrix to rotate 2D vector by the angle.
:param angle: angle in radians
"""
return np.array(
[[np.cos(angle), np.sin(angle)], [-np.sin(angle), np.cos(angle)]]
)
class Sector:
"""Sector described by two vectors."""
def __init__(self, text: str, angle: Optional[float] = None) -> None:
"""
:param text: sector text representation (e.g. "70-210", "N-NW")
:param angle: angle in degrees
"""
self.start: Optional[np.ndarray] = None
self.end: Optional[np.ndarray] = None
self.main_direction: Optional[np.ndarray] = None
if "-" in text and not text.startswith("-"):
parts: list[str] = text.split("-")
self.start = parse_vector(parts[0])
self.end = parse_vector(parts[1])
self.main_direction = (self.start + self.end) / 2.0
else:
result_angle: float
if angle is None:
result_angle = DEFAULT_ANGLE
else:
result_angle = max(SMALLEST_ANGLE, np.radians(angle) / 2.0)
vector: Optional[np.ndarray] = parse_vector(text)
self.main_direction = vector
if vector is not None:
self.start = np.dot(rotation_matrix(result_angle), vector)
self.end = np.dot(rotation_matrix(-result_angle), vector)
def draw(self, center: np.ndarray, radius: float) -> Optional[PathCommands]:
"""
Construct SVG path commands for arc element.
:param center: arc center point
:param radius: arc radius
:return: SVG path commands
"""
if self.start is None or self.end is None:
return None
start: np.ndarray = center + radius * self.end
end: np.ndarray = center + radius * self.start
return ["L", start, "A", radius, radius, 0, "0", 0, end]
def is_right(self) -> Optional[bool]:
"""
Check if main direction of the sector is right.
:return: true if direction is right, false if direction is left, and
None otherwise.
"""
if self.main_direction is not None:
if np.allclose(self.main_direction[0], 0.0):
return None
if self.main_direction[0] > 0.0:
return True
return False
return None
def __str__(self) -> str:
return f"{self.start}-{self.end}"
class DirectionSet:
"""Describes direction, set of directions."""
def __init__(self, text: str) -> None:
"""
:param text: direction tag value
"""
self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";"))
def __str__(self) -> str:
return ", ".join(map(str, self.sectors))
def draw(self, center: np.ndarray, radius: float) -> Iterator[PathCommands]:
"""
Construct SVG "d" for arc elements.
:param center: center point of all arcs
:param radius: radius of all arcs
:return: list of "d" values
"""
return filter(
lambda x: x is not None,
map(lambda x: x.draw(center, radius), self.sectors),
)
def is_right(self) -> Optional[bool]:
"""
Check if main direction of the sector is right.
:return: true if direction is right, false if direction is left, and
None otherwise.
"""
result: list[bool] = [sector.is_right() for sector in self.sectors]
if result == [True] * len(result):
return True
if result == [False] * len(result):
return False
return None
class DirectionSector(Tagged):
"""Sector that represents direction."""
def __init__(self, tags: dict[str, str], point: np.ndarray) -> None:
super().__init__(tags)
self.point: np.ndarray = point
def draw(self, svg: Drawing, scheme) -> None:
"""Draw gradient sector."""
angle: Optional[float] = None
is_revert_gradient: bool = False
direction: str
direction_radius: float
direction_color: Color
if self.get_tag("man_made") == "surveillance":
direction = self.get_tag("camera:direction")
if "camera:angle" in self.tags:
angle = float(self.get_tag("camera:angle"))
if "angle" in self.tags:
angle = float(self.get_tag("angle"))
direction_radius = 50.0
direction_color = scheme.get_color("direction_camera_color")
elif self.get_tag("traffic_sign") == "stop":
direction = self.get_tag("direction")
direction_radius = 25.0
direction_color = Color("red")
else:
direction = self.get_tag("direction")
direction_radius = 50.0
direction_color = scheme.get_color("direction_view_color")
is_revert_gradient = True
if not direction:
return
point: np.ndarray = (self.point.astype(int)).astype(float)
paths: Iterator[PathCommands]
if angle is not None:
paths = [Sector(direction, angle).draw(point, direction_radius)]
else:
paths = DirectionSet(direction).draw(point, direction_radius)
for path in paths:
radial_gradient: RadialGradient = svg.radialGradient(
center=point,
r=direction_radius,
gradientUnits="userSpaceOnUse",
)
gradient: RadialGradient = svg.defs.add(radial_gradient)
if is_revert_gradient:
(
gradient
.add_stop_color(0.0, direction_color.hex, opacity=0.0)
.add_stop_color(1.0, direction_color.hex, opacity=0.7)
) # fmt: skip
else:
(
gradient
.add_stop_color(0.0, direction_color.hex, opacity=0.4)
.add_stop_color(1.0, direction_color.hex, opacity=0.0)
) # fmt: skip
path_element: Path = svg.path(
d=["M", point] + path + ["L", point, "Z"],
fill=gradient.get_funciri(),
)
svg.add(path_element)
class Segment:
"""Closed line segment."""
def __init__(self, point_1: np.ndarray, point_2: np.ndarray) -> None:
self.point_1: np.ndarray = point_1
self.point_2: np.ndarray = point_2
difference: np.ndarray = point_2 - point_1
vector: np.ndarray = difference / np.linalg.norm(difference)
self.angle: float = (
np.arccos(np.dot(vector, np.array((0.0, 1.0)))) / np.pi
)
def __lt__(self, other: "Segment") -> bool:
return (
((self.point_1 + self.point_2) / 2.0)[1]
< ((other.point_1 + other.point_2) / 2.0)[1]
) # fmt: skip