Refactor type annotaions; get rid of minidom.

This commit is contained in:
Sergey Vartanov 2021-08-28 03:19:51 +03:00
parent 81502e542e
commit cdeffc8758
22 changed files with 401 additions and 353 deletions

View file

@ -4,6 +4,7 @@ Rectangle that limit space on the map.
import logging
import re
from dataclasses import dataclass
from typing import Optional
import numpy as np
@ -26,7 +27,7 @@ class BoundaryBox:
top: float # Maximum latitude.
@classmethod
def from_text(cls, boundary_box: str):
def from_text(cls, boundary_box: str) -> "BoundaryBox":
"""
Parse boundary box string representation.
@ -41,7 +42,7 @@ class BoundaryBox:
"""
boundary_box = boundary_box.replace(" ", "")
matcher = re.match(
matcher: Optional[re.Match] = re.match(
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
boundary_box,

View file

@ -17,11 +17,13 @@ from roentgen.map_configuration import DrawingMode, MapConfiguration
# fmt: off
from roentgen.icon import (
DEFAULT_SMALL_SHAPE_ID, Icon, IconSet, ShapeExtractor, ShapeSpecification
DEFAULT_SMALL_SHAPE_ID, Icon, IconSet, Shape, ShapeExtractor,
ShapeSpecification
)
from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay
from roentgen.point import Point
from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme
from roentgen.scheme import DEFAULT_COLOR, LineStyle, RoadMatcher, Scheme
from roentgen.text import Label
from roentgen.ui import BuildingMode
from roentgen.util import MinMax
# fmt: on
@ -40,7 +42,9 @@ TIME_COLOR_SCALE: list[Color] = [
]
def line_center(nodes: list[OSMNode], flinger: Flinger) -> np.array:
def line_center(
nodes: list[OSMNode], flinger: Flinger
) -> (np.ndarray, np.ndarray):
"""
Get geometric center of nodes set.
@ -52,8 +56,9 @@ def line_center(nodes: list[OSMNode], flinger: Flinger) -> np.array:
for node in nodes:
boundary[0].update(node.coordinates[0])
boundary[1].update(node.coordinates[1])
center_coordinates = np.array((boundary[0].center(), boundary[1].center()))
center_coordinates: np.ndarray = np.array(
(boundary[0].center(), boundary[1].center())
)
return flinger.fling(center_coordinates), center_coordinates
@ -97,7 +102,7 @@ def glue(ways: list[OSMWay]) -> list[list[OSMNode]]:
other_way: Optional[OSMWay] = None
for other_way in to_process:
glued = way.try_to_glue(other_way)
glued: Optional[OSMWay] = way.try_to_glue(other_way)
if glued:
break
@ -128,14 +133,14 @@ class Constructor:
osm_data: OSMData,
flinger: Flinger,
scheme: Scheme,
icon_extractor: ShapeExtractor,
extractor: ShapeExtractor,
configuration: MapConfiguration,
) -> None:
self.osm_data: OSMData = osm_data
self.flinger: Flinger = flinger
self.scheme: Scheme = scheme
self.icon_extractor = icon_extractor
self.configuration = configuration
self.extractor: ShapeExtractor = extractor
self.configuration: MapConfiguration = configuration
if self.configuration.level:
if self.configuration.level == "overground":
@ -209,7 +214,7 @@ class Constructor:
color = get_user_color(line.user, self.configuration.seed)
else: # self.mode == TIME_MODE
color = get_time_color(line.timestamp, self.osm_data.time)
self.draw_special_mode(inners, line, outers, color)
self.draw_special_mode(line, inners, outers, color)
return
if not line.tags:
@ -224,7 +229,7 @@ class Constructor:
Building(line.tags, inners, outers, self.flinger, self.scheme)
)
road_matcher = self.scheme.get_road(line.tags)
road_matcher: RoadMatcher = self.scheme.get_road(line.tags)
if road_matcher:
self.roads.append(Road(line.tags, inners, outers, road_matcher))
return
@ -246,16 +251,17 @@ class Constructor:
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.icon_extractor, line.tags, processed
self.extractor, line.tags, processed
)
labels: list[Label] = self.scheme.construct_text(
line.tags, "all", processed
)
labels = self.scheme.construct_text(line.tags, "all", processed)
point: Point = Point(
icon_set,
labels,
line.tags,
processed,
center_point,
center_coordinates,
is_for_node=False,
priority=priority,
) # fmt: skip
@ -278,16 +284,24 @@ class Constructor:
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.icon_extractor, line.tags, processed
self.extractor, line.tags, processed
)
labels: list[Label] = self.scheme.construct_text(
line.tags, "all", processed
)
labels = self.scheme.construct_text(line.tags, "all", processed)
point: Point = Point(
icon_set, labels, line.tags, processed, center_point,
center_coordinates, is_for_node=False, priority=priority,
is_for_node=False, priority=priority,
) # fmt: skip
self.points.append(point)
def draw_special_mode(self, inners, line, outers, color) -> None:
def draw_special_mode(
self,
line: Union[OSMWay, OSMRelation],
inners: list[list[OSMNode]],
outers: list[list[OSMNode]],
color: Color,
) -> None:
"""Add figure for special mode: time or author."""
style: dict[str, Any] = {
"fill": "none",
@ -302,7 +316,7 @@ class Constructor:
"""Construct Röntgen ways from OSM relations."""
for relation_id in self.osm_data.relations:
relation: OSMRelation = self.osm_data.relations[relation_id]
tags = relation.tags
tags: dict[str, str] = relation.tags
if not self.check_level(tags):
continue
if "type" not in tags or tags["type"] != "multipolygon":
@ -340,13 +354,13 @@ class Constructor:
def construct_node(self, node: OSMNode) -> None:
"""Draw one node."""
tags = node.tags
tags: dict[str, str] = node.tags
if not self.check_level(tags):
return
processed: set[str] = set()
flung = self.flinger.fling(node.coordinates)
flung: np.ndarray = self.flinger.fling(node.coordinates)
priority: int
icon_set: IconSet
@ -360,21 +374,20 @@ class Constructor:
color = get_user_color(node.user, self.configuration.seed)
if self.configuration.drawing_mode == DrawingMode.TIME:
color = get_time_color(node.timestamp, self.osm_data.time)
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
icon_set = IconSet(
dot: Shape = self.extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
icon_set: IconSet = IconSet(
Icon([ShapeSpecification(dot, color)]), [], set()
)
point: Point = Point(
icon_set, [], tags, processed, flung, node.coordinates,
draw_outline=False
) # fmt: skip
icon_set, [], tags, processed, flung, draw_outline=False
)
self.points.append(point)
return
icon_set, priority = self.scheme.get_icon(
self.icon_extractor, tags, processed
self.extractor, tags, processed
)
labels = self.scheme.construct_text(tags, "all", processed)
labels: list[Label] = self.scheme.construct_text(tags, "all", processed)
self.scheme.process_ignored(tags, processed)
if node.get_tag("natural") == "tree" and (
@ -386,16 +399,16 @@ class Constructor:
if "direction" in node.tags or "camera:direction" in node.tags:
self.direction_sectors.append(DirectionSector(tags, flung))
point: Point = Point(
icon_set, labels, tags, processed, flung, node.coordinates,
icon_set, labels, tags, processed, flung,
priority=priority, draw_outline=draw_outline
) # fmt: skip
self.points.append(point)
def check_level_number(tags: dict[str, Any], level: float):
def check_level_number(tags: dict[str, Any], level: float) -> bool:
"""Check if element described by tags is no the specified level."""
if "level" in tags:
levels = map(float, tags["level"].replace(",", ".").split(";"))
levels: map = map(float, tags["level"].replace(",", ".").split(";"))
if level not in levels:
return False
else:
@ -407,7 +420,7 @@ def check_level_overground(tags: dict[str, Any]) -> bool:
"""Check if element described by tags is overground."""
if "level" in tags:
try:
levels = map(float, tags["level"].replace(",", ".").split(";"))
levels: map = map(float, tags["level"].replace(",", ".").split(";"))
for level in levels:
if level <= 0:
return False
@ -415,7 +428,7 @@ def check_level_overground(tags: dict[str, Any]) -> bool:
pass
if "layer" in tags:
try:
levels = map(float, tags["layer"].replace(",", ".").split(";"))
levels: map = map(float, tags["layer"].replace(",", ".").split(";"))
for level in levels:
if level <= 0:
return False

View file

@ -16,12 +16,7 @@ SMALLEST_ANGLE: float = np.pi / 15
DEFAULT_ANGLE: float = np.pi / 30
def degree_to_radian(degree: float) -> float:
"""Convert value in degrees to radians."""
return degree / 180 * np.pi
def parse_vector(text: str) -> Optional[np.array]:
def parse_vector(text: str) -> Optional[np.ndarray]:
"""
Parse vector from text representation: compass points or 360-degree
notation. E.g. "NW", "270".
@ -30,13 +25,13 @@ def parse_vector(text: str) -> Optional[np.array]:
:return: parsed normalized vector
"""
try:
radians: float = degree_to_radian(float(text)) + SHIFT
radians: float = np.radians(float(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except ValueError:
pass
try:
radians: float = degree_to_radian(middle(text)) + SHIFT
radians: float = np.radians(middle(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except KeyError:
pass
@ -44,7 +39,7 @@ def parse_vector(text: str) -> Optional[np.array]:
return None
def rotation_matrix(angle) -> np.array:
def rotation_matrix(angle: float) -> np.ndarray:
"""
Get a matrix to rotate 2D vector by the angle.
@ -65,9 +60,9 @@ class Sector:
:param text: sector text representation (e.g. "70-210", "N-NW")
:param angle: angle in degrees
"""
self.start: Optional[np.array] = None
self.end: Optional[np.array] = None
self.main_direction: Optional[np.array] = None
self.start: Optional[np.ndarray] = None
self.end: Optional[np.ndarray] = None
self.main_direction: Optional[np.ndarray] = None
if "-" in text:
parts: list[str] = text.split("-")
@ -79,16 +74,16 @@ class Sector:
if angle is None:
result_angle = DEFAULT_ANGLE
else:
result_angle = max(SMALLEST_ANGLE, degree_to_radian(angle) / 2)
result_angle = max(SMALLEST_ANGLE, np.radians(angle) / 2)
vector: Optional[np.array] = parse_vector(text)
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.array, radius: float) -> Optional[PathCommands]:
def draw(self, center: np.ndarray, radius: float) -> Optional[PathCommands]:
"""
Construct SVG path commands for arc element.
@ -99,8 +94,8 @@ class Sector:
if self.start is None or self.end is None:
return None
start: np.array = center + radius * self.end
end: np.array = center + radius * self.start
start: np.ndarray = center + radius * self.end
end: np.ndarray = center + radius * self.start
return ["L", start, "A", radius, radius, 0, "0", 0, end]
@ -137,7 +132,7 @@ class DirectionSet:
def __str__(self) -> str:
return ", ".join(map(str, self.sectors))
def draw(self, center: np.array, radius: float) -> Iterator[PathCommands]:
def draw(self, center: np.ndarray, radius: float) -> Iterator[PathCommands]:
"""
Construct SVG "d" for arc elements.

View file

@ -9,6 +9,8 @@ import cairo
import numpy as np
import svgwrite
from colour import Color
from cairo import Context, ImageSurface
from svgwrite.base import BaseElement
from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect
from svgwrite.text import Text
@ -29,7 +31,7 @@ class Style:
stroke: Optional[Color] = None
width: float = 1
def update_svg_element(self, element) -> None:
def update_svg_element(self, element: BaseElement) -> None:
"""Set style for SVG element."""
if self.fill is not None:
element.update({"fill": self.fill})
@ -38,14 +40,14 @@ class Style:
if self.stroke is not None:
element.update({"stroke": self.stroke, "stroke-width": self.width})
def draw_png_fill(self, context) -> None:
def draw_png_fill(self, context: Context) -> None:
"""Set style for context and draw fill."""
context.set_source_rgba(
self.fill.get_red(), self.fill.get_green(), self.fill.get_blue(), 1
)
context.fill()
def draw_png_stroke(self, context) -> None:
def draw_png_stroke(self, context: Context) -> None:
"""Set style for context and draw stroke."""
context.set_source_rgba(
self.stroke.get_red(),
@ -81,7 +83,9 @@ class Drawing:
"""Draw path."""
raise NotImplementedError
def text(self, text: str, point: np.ndarray, color: Color = Color("black")):
def text(
self, text: str, point: np.ndarray, color: Color = Color("black")
) -> None:
"""Draw text."""
raise NotImplementedError
@ -97,7 +101,9 @@ class SVGDrawing(Drawing):
def __init__(self, file_path: Path, width: int, height: int) -> None:
super().__init__(file_path, width, height)
self.image = svgwrite.Drawing(str(file_path), (width, height))
self.image: svgwrite.Drawing = svgwrite.Drawing(
str(file_path), (width, height)
)
def rectangle(
self, point_1: np.ndarray, point_2: np.ndarray, style: Style
@ -120,11 +126,13 @@ class SVGDrawing(Drawing):
def path(self, commands: PathCommands, style: Style) -> None:
"""Draw path."""
path = SVGPath(d=commands)
path: SVGPath = SVGPath(d=commands)
style.update_svg_element(path)
self.image.add(path)
def text(self, text: str, point: np.ndarray, color: Color = Color("black")):
def text(
self, text: str, point: np.ndarray, color: Color = Color("black")
) -> None:
"""Draw text."""
self.image.add(
Text(text, (float(point[0]), float(point[1])), fill=color)
@ -143,8 +151,10 @@ class PNGDrawing(Drawing):
def __init__(self, file_path: Path, width: int, height: int) -> None:
super().__init__(file_path, width, height)
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
self.context = cairo.Context(self.surface)
self.surface: ImageSurface = ImageSurface(
cairo.FORMAT_ARGB32, width, height
)
self.context: Context = Context(self.surface)
def rectangle(
self, point_1: np.ndarray, point_2: np.ndarray, style: Style
@ -181,7 +191,7 @@ class PNGDrawing(Drawing):
index: int = 0
while index < len(commands):
element = commands[index]
element: Union[float, str, np.ndarray] = commands[index]
if isinstance(element, str):
is_absolute: bool = element.lower() != element
@ -192,10 +202,11 @@ class PNGDrawing(Drawing):
start_point = None
elif command in "ml":
point: np.ndarray
if is_absolute:
point: np.ndarray = commands[index]
point = commands[index]
else:
point: np.ndarray = current + commands[index]
point = current + commands[index]
current = point
if command == "m":
self.context.move_to(point[0], point[1])
@ -225,6 +236,7 @@ class PNGDrawing(Drawing):
elif command in "vh":
assert isinstance(commands[index], float)
point: np.ndarray
if is_absolute:
if command == "v":
point = np.array((0, commands[index]))
@ -254,7 +266,9 @@ class PNGDrawing(Drawing):
self._do_path(commands)
style.draw_png_stroke(self.context)
def text(self, text: str, point: np.ndarray, color: Color = Color("black")):
def text(
self, text: str, point: np.ndarray, color: Color = Color("black")
) -> None:
"""Draw text."""
self.context.set_source_rgb(
color.get_red(), color.get_green(), color.get_blue()
@ -282,7 +296,7 @@ def parse_path(path: str) -> PathCommands:
result.append(float(part))
else:
if "," in part:
elements = part.split(",")
elements: list[str] = part.split(",")
result.append(np.array(list(map(float, elements))))
else:
result.append(np.array((float(part), float(parts[index + 1]))))

View file

@ -7,10 +7,12 @@ from pathlib import Path
import numpy as np
import svgwrite
from svgwrite.path import Path as SVGPath
from roentgen.icon import ShapeExtractor
from roentgen.point import Point
from roentgen.scheme import LineStyle, Scheme
from roentgen.text import Label
from roentgen.workspace import workspace
__author__ = "Sergey Vartanov"
@ -19,12 +21,17 @@ __email__ = "me@enzet.ru"
def draw_element(options: argparse.Namespace) -> None:
"""Draw single node, line, or area."""
target: str
tags_description: str
if options.node:
target: str = "node"
target = "node"
tags_description = options.node
elif options.way:
target = "way"
tags_description = options.way
else:
# Not implemented yet.
exit(1)
target = "area"
tags_description = options.area
tags: dict[str, str] = dict(
[x.split("=") for x in tags_description.split(",")]
@ -36,26 +43,27 @@ def draw_element(options: argparse.Namespace) -> None:
processed: set[str] = set()
icon, priority = scheme.get_icon(extractor, tags, processed)
is_for_node: bool = target == "node"
labels = scheme.construct_text(tags, "all", processed)
point = Point(
labels: list[Label] = scheme.construct_text(tags, "all", processed)
point: Point = Point(
icon,
labels,
tags,
processed,
np.array((32, 32)),
None,
is_for_node=is_for_node,
draw_outline=is_for_node,
)
border: np.array = np.array((16, 16))
size: np.array = point.get_size() + border
border: np.ndarray = np.array((16, 16))
size: np.ndarray = point.get_size() + border
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
output_file_path: Path = workspace.output_path / "element.svg"
svg = svgwrite.Drawing(str(output_file_path), size.astype(float))
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_file_path), size.astype(float)
)
for style in scheme.get_style(tags):
style: LineStyle
path = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
path: SVGPath = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
path.update(style.style)
svg.add(path)
point.draw_main_shapes(svg)

View file

@ -1,14 +1,16 @@
"""
Figures displayed on the map.
"""
from typing import Any, Optional
from typing import Any, Iterator, Optional
import numpy as np
from colour import Color
from svgwrite import Drawing
from svgwrite.container import Group
from svgwrite.path import Path
from roentgen.direction import DirectionSet, Sector
from roentgen.drawing import PathCommands
from roentgen.flinger import Flinger
from roentgen.osm_reader import OSMNode, Tagged
from roentgen.road import Lane
@ -41,7 +43,7 @@ class Figure(Tagged):
self.outers.append(make_counter_clockwise(outer_nodes))
def get_path(
self, flinger: Flinger, shift: np.array = np.array((0, 0))
self, flinger: Flinger, shift: np.ndarray = np.array((0, 0))
) -> str:
"""
Get SVG path commands.
@ -79,13 +81,13 @@ class Building(Figure):
"fill": scheme.get_color("building_color").hex,
"stroke": scheme.get_color("building_border_color").hex,
}
self.line_style = LineStyle(style)
self.parts = []
self.line_style: LineStyle = LineStyle(style)
self.parts: list[Segment] = []
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)
flung_1: np.ndarray = flinger.fling(nodes[i].coordinates)
flung_2: np.ndarray = flinger.fling(nodes[i + 1].coordinates)
self.parts.append(Segment(flung_1, flung_2))
self.parts = sorted(self.parts)
@ -109,20 +111,20 @@ class Building(Figure):
if height:
self.min_height = height
def draw(self, svg: Drawing, flinger: Flinger):
def draw(self, svg: Drawing, flinger: Flinger) -> None:
"""Draw simple building shape."""
path: Path = Path(d=self.get_path(flinger))
path.update(self.line_style.style)
path.update({"stroke-linejoin": "round"})
svg.add(path)
def draw_shade(self, building_shade, flinger: Flinger) -> None:
def draw_shade(self, building_shade: Group, flinger: Flinger) -> None:
"""Draw shade casted by the building."""
scale: float = flinger.get_scale() / 3.0
shift_1 = np.array((scale * self.min_height, 0))
shift_2 = np.array((scale * self.height, 0))
shift_1: np.ndarray = np.array((scale * self.min_height, 0))
shift_2: np.ndarray = np.array((scale * self.height, 0))
commands: str = self.get_path(flinger, shift_1)
path = Path(
path: Path = Path(
d=commands, fill="#000000", stroke="#000000", stroke_width=1
)
building_shade.add(path)
@ -130,7 +132,7 @@ class Building(Figure):
for i in range(len(nodes) - 1):
flung_1 = flinger.fling(nodes[i].coordinates)
flung_2 = flinger.fling(nodes[i + 1].coordinates)
command = (
command: PathCommands = [
"M",
np.add(flung_1, shift_1),
"L",
@ -138,8 +140,8 @@ class Building(Figure):
np.add(flung_2, shift_2),
np.add(flung_1, shift_2),
"Z",
)
path = Path(
]
path: Path = Path(
command, fill="#000000", stroke="#000000", stroke_width=1
)
building_shade.add(path)
@ -148,9 +150,10 @@ class Building(Figure):
self, svg: Drawing, height: float, previous_height: float, scale: float
) -> None:
"""Draw building walls."""
shift_1 = [0, -previous_height * scale]
shift_2 = [0, -height * scale]
shift_1: np.ndarray = np.array((0, -previous_height * scale))
shift_2: np.ndarray = np.array((0, -height * scale))
for segment in self.parts:
fill: Color
if height == 2:
fill = Color("#AAAAAA")
elif height == 4:
@ -169,7 +172,7 @@ class Building(Figure):
segment.point_1 + shift_1,
"Z",
)
path = svg.path(
path: Path = svg.path(
d=command,
fill=fill.hex,
stroke=fill.hex,
@ -178,7 +181,7 @@ class Building(Figure):
)
svg.add(path)
def draw_roof(self, svg: Drawing, flinger: Flinger, scale: float):
def draw_roof(self, svg: Drawing, flinger: Flinger, scale: float) -> None:
"""Draw building roof."""
path: Path = Path(
d=self.get_path(flinger, np.array([0, -self.height * scale]))
@ -201,7 +204,7 @@ class StyledFigure(Figure):
line_style: LineStyle,
) -> None:
super().__init__(tags, inners, outers)
self.line_style = line_style
self.line_style: LineStyle = line_style
class Road(Figure):
@ -229,6 +232,7 @@ class Road(Figure):
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:]]
@ -249,13 +253,13 @@ class Tree(Tagged):
"""
def __init__(
self, tags: dict[str, str], coordinates: np.array, point: np.array
self, tags: dict[str, str], coordinates: np.ndarray, point: np.ndarray
) -> None:
super().__init__(tags)
self.coordinates: np.array = coordinates
self.point: np.array = point
self.coordinates: np.ndarray = coordinates
self.point: np.ndarray = point
def draw(self, svg: Drawing, flinger: Flinger, scheme: Scheme):
def draw(self, svg: Drawing, flinger: Flinger, scheme: Scheme) -> None:
"""Draw crown and trunk."""
scale: float = flinger.get_scale(self.coordinates)
radius: float
@ -276,14 +280,17 @@ class DirectionSector(Tagged):
Sector that represents direction.
"""
def __init__(self, tags: dict[str, str], point) -> None:
def __init__(self, tags: dict[str, str], point: np.ndarray) -> None:
super().__init__(tags)
self.point = point
self.point: np.ndarray = point
def draw(self, svg: Drawing, scheme: Scheme):
def draw(self, svg: Drawing, scheme: Scheme) -> None:
"""Draw gradient sector."""
angle = None
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")
@ -291,24 +298,25 @@ class DirectionSector(Tagged):
angle = float(self.get_tag("camera:angle"))
if "angle" in self.tags:
angle = float(self.get_tag("angle"))
direction_radius: float = 25
direction_color: Color = scheme.get_color("direction_camera_color")
direction_radius = 25
direction_color = scheme.get_color("direction_camera_color")
elif self.get_tag("traffic_sign") == "stop":
direction = self.get_tag("direction")
direction_radius: float = 25
direction_color: Color = Color("red")
direction_radius = 25
direction_color = Color("red")
else:
direction = self.get_tag("direction")
direction_radius: float = 50
direction_color: Color = scheme.get_color("direction_view_color")
direction_radius = 50
direction_color = scheme.get_color("direction_view_color")
is_revert_gradient = True
if not direction:
return
point = (self.point.astype(int)).astype(float)
point: np.ndarray = (self.point.astype(int)).astype(float)
if angle:
paths: Iterator[PathCommands]
if angle is not None:
paths = [Sector(direction, angle).draw(point, direction_radius)]
else:
paths = DirectionSet(direction).draw(point, direction_radius)
@ -344,12 +352,12 @@ class Segment:
Line segment.
"""
def __init__(self, point_1: np.array, point_2: np.array) -> None:
self.point_1: np.array = point_1
self.point_2: np.array = point_2
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.array = point_2 - point_1
vector: np.array = difference / np.linalg.norm(difference)
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, 1)))) / np.pi
def __lt__(self, other: "Segment") -> bool:
@ -392,12 +400,12 @@ def make_counter_clockwise(polygon: list[OSMNode]) -> list[OSMNode]:
return polygon if not is_clockwise(polygon) else list(reversed(polygon))
def get_path(nodes: list[OSMNode], shift: np.array, flinger: Flinger) -> str:
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 = flinger.fling(node.coordinates) + shift
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]:

View file

@ -13,7 +13,7 @@ __email__ = "me@enzet.ru"
EQUATOR_LENGTH: float = 40_075_017 # meters
def pseudo_mercator(coordinates: np.array) -> np.array:
def pseudo_mercator(coordinates: np.ndarray) -> np.ndarray:
"""
Use spherical pseudo-Mercator projection to convert geo coordinates into
plane.
@ -47,32 +47,33 @@ class Flinger:
self,
geo_boundaries: BoundaryBox,
scale: float = 18,
border: np.array = np.array((0, 0)),
border: np.ndarray = np.array((0, 0)),
) -> None:
"""
:param geo_boundaries: minimum and maximum latitude and longitude
:param scale: OSM zoom level
:param border: size of padding in pixels
"""
self.geo_boundaries: BoundaryBox = geo_boundaries
self.border = border
self.border: np.ndarray = border
self.ratio: float = (
osm_zoom_level_to_pixels_per_meter(scale) * EQUATOR_LENGTH / 360
)
self.size: np.array = border * 2 + self.ratio * (
self.size: np.ndarray = border * 2 + self.ratio * (
pseudo_mercator(self.geo_boundaries.max_())
- pseudo_mercator(self.geo_boundaries.min_())
)
self.pixels_per_meter = osm_zoom_level_to_pixels_per_meter(scale)
self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter(scale)
self.size: np.array = self.size.astype(int).astype(float)
self.size: np.ndarray = self.size.astype(int).astype(float)
def fling(self, coordinates: np.array) -> np.array:
def fling(self, coordinates: np.ndarray) -> np.ndarray:
"""
Convert geo coordinates into SVG position points.
:param coordinates: vector to fling
"""
result: np.array = self.border + self.ratio * (
result: np.ndarray = self.border + self.ratio * (
pseudo_mercator(coordinates)
- pseudo_mercator(self.geo_boundaries.min_())
)
@ -82,7 +83,7 @@ class Flinger:
return result
def get_scale(self, coordinates: Optional[np.array] = None) -> float:
def get_scale(self, coordinates: Optional[np.ndarray] = None) -> float:
"""
Return pixels per meter ratio for the given geo coordinates.

View file

@ -4,11 +4,12 @@ Icon grid drawing.
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from typing import Optional, Union
import numpy as np
from colour import Color
from svgwrite import Drawing
from svgwrite.shapes import Rect
from roentgen.icon import Icon, Shape, ShapeExtractor, ShapeSpecification
from roentgen.scheme import NodeMatcher, Scheme
@ -51,8 +52,8 @@ class IconCollection:
def add() -> Icon:
"""Construct icon and add it to the list."""
specifications = [
ShapeSpecification.from_structure(x, extractor, scheme)
specifications: list[ShapeSpecification] = [
scheme.get_shape_specification(x, extractor)
for x in current_set
]
constructed_icon: Icon = Icon(specifications)
@ -62,6 +63,8 @@ class IconCollection:
return constructed_icon
current_set: list[Union[str, dict[str, str]]]
for matcher in scheme.node_matchers:
matcher: NodeMatcher
if matcher.shapes:
@ -129,7 +132,7 @@ class IconCollection:
color: Optional[Color] = None,
outline: bool = False,
outline_opacity: float = 1.0,
):
) -> None:
"""
:param output_directory: path to the directory to store individual SVG
files for icons
@ -140,13 +143,13 @@ class IconCollection:
"""
if by_name:
def get_file_name(x) -> str:
def get_file_name(x: Icon) -> str:
"""Generate human-readable file name."""
return f"Röntgen {' + '.join(x.get_names())}.svg"
else:
def get_file_name(x) -> str:
def get_file_name(x: Icon) -> str:
"""Generate file name with unique identifier."""
return f"{'___'.join(x.get_shape_ids())}.svg"
@ -164,7 +167,7 @@ class IconCollection:
columns: int = 16,
step: float = 24,
background_color: Color = Color("white"),
):
) -> None:
"""
Draw icons in the form of table.
@ -173,7 +176,7 @@ class IconCollection:
:param step: horizontal and vertical distance between icons in grid
:param background_color: background color
"""
point: np.array = np.array((step / 2, step / 2))
point: np.ndarray = np.array((step / 2, step / 2))
width: float = step * columns
height: int = int(int(len(self.icons) / (width / step) + 1) * step)
@ -182,7 +185,7 @@ class IconCollection:
for icon in self.icons:
icon: Icon
rectangle = svg.rect(
rectangle: Rect = svg.rect(
point - np.array((10, 10)), (20, 20), fill=background_color.hex
)
svg.add(rectangle)

View file

@ -7,14 +7,15 @@ import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
from xml.dom.minidom import Document, Element, parse
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
import numpy as np
import svgwrite
from colour import Color
from svgwrite import Drawing
from svgwrite.container import Group
from svgwrite.path import Path as SvgPath
from svgwrite.path import Path as SVGPath
from roentgen.color import is_bright
@ -25,10 +26,11 @@ DEFAULT_COLOR: Color = Color("#444444")
DEFAULT_SHAPE_ID: str = "default"
DEFAULT_SMALL_SHAPE_ID: str = "default_small"
STANDARD_INKSCAPE_ID_MATCHER = re.compile(
"^((circle|defs|ellipse|metadata|path|rect|use)[\\d-]+|base)$"
STANDARD_INKSCAPE_ID_MATCHER: re.Pattern = re.compile(
"^((circle|defs|ellipse|grid|guide|marker|metadata|path|rect|use)"
"[\\d-]+|base)$"
)
PATH_MATCHER = re.compile("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)")
PATH_MATCHER: re.Pattern = re.compile("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)")
GRID_STEP: int = 16
@ -40,7 +42,7 @@ class Shape:
"""
path: str # SVG icon path
offset: np.array # vector that should be used to shift the path
offset: np.ndarray # vector that should be used to shift the path
id_: str # shape identifier
name: Optional[str] = None # icon description
is_right_directed: Optional[bool] = None
@ -52,7 +54,7 @@ class Shape:
cls,
structure: dict[str, Any],
path: str,
offset: np.array,
offset: np.ndarray,
id_: str,
name: Optional[str] = None,
) -> "Shape":
@ -65,7 +67,7 @@ class Shape:
:param id_: shape unique identifier
:param name: shape text description
"""
shape = cls(path, offset, id_, name)
shape: "Shape" = cls(path, offset, id_, name)
if "directed" in structure:
if structure["directed"] == "right":
@ -91,10 +93,10 @@ class Shape:
def get_path(
self,
point: np.array,
offset: np.array = np.array((0, 0)),
scale: np.array = np.array((1, 1)),
) -> SvgPath:
point: np.ndarray,
offset: np.ndarray = np.array((0, 0)),
scale: np.ndarray = np.array((1, 1)),
) -> SVGPath:
"""
Draw icon into SVG file.
@ -103,7 +105,7 @@ class Shape:
:param scale: scale resulting image
"""
transformations: list[str] = []
shift: np.array = point + offset
shift: np.ndarray = point + offset
transformations.append(f"translate({shift[0]},{shift[1]})")
@ -124,7 +126,7 @@ def parse_length(text: str) -> float:
return float(text)
def verify_sketch_element(element, id_: str) -> bool:
def verify_sketch_element(element: Element, id_: str) -> bool:
"""
Verify sketch SVG element from icon file.
@ -132,11 +134,11 @@ def verify_sketch_element(element, id_: str) -> bool:
:param id_: element `id` attribute
:return: True iff SVG element has right style
"""
if not element.getAttribute("style"):
if "style" not in element.attrib or not element.attrib["style"]:
return True
style: dict = dict(
[x.split(":") for x in element.getAttribute("style").split(";")]
style: dict[str, str] = dict(
[x.split(":") for x in element.attrib["style"].split(";")]
)
if (
style["fill"] == "none"
@ -181,14 +183,8 @@ class ShapeExtractor:
self.configuration: dict[str, Any] = json.load(
configuration_file_name.open()
)
with svg_file_name.open() as input_file:
content: Document = parse(input_file)
for element in content.childNodes:
if element.nodeName != "svg":
continue
for node in element.childNodes:
if isinstance(node, Element):
self.parse(node)
root: Element = ElementTree.parse(svg_file_name).getroot()
self.parse(root)
def parse(self, node: Element) -> None:
"""
@ -196,41 +192,40 @@ class ShapeExtractor:
:param node: XML node that contains icon
"""
if node.nodeName == "g":
for sub_node in node.childNodes:
if isinstance(sub_node, Element):
if node.tag.endswith("}g") or node.tag.endswith("}svg"):
for sub_node in node:
self.parse(sub_node)
return
if not node.hasAttribute("id") or not node.getAttribute("id"):
if "id" not in node.attrib or not node.attrib["id"]:
return
id_: str = node.getAttribute("id")
id_: str = node.attrib["id"]
if STANDARD_INKSCAPE_ID_MATCHER.match(id_) is not None:
if not verify_sketch_element(node, id_):
logging.warning(f"Not verified SVG element `{id_}`.")
return
if node.hasAttribute("d"):
path: str = node.getAttribute("d")
if "d" in node.attrib and node.attrib["d"]:
path: str = node.attrib["d"]
matcher = PATH_MATCHER.match(path)
if not matcher:
return
name: Optional[str] = None
def get_offset(value: str):
def get_offset(value: str) -> float:
"""Get negated icon offset from the origin."""
return (
-int(float(value) / GRID_STEP) * GRID_STEP - GRID_STEP / 2
)
point: np.array = np.array(
point: np.ndarray = np.array(
(get_offset(matcher.group(1)), get_offset(matcher.group(2)))
)
for child_node in node.childNodes:
for child_node in node:
if isinstance(child_node, Element):
name = child_node.childNodes[0].nodeValue
name = child_node.text
break
configuration: dict[str, Any] = (
@ -242,7 +237,7 @@ class ShapeExtractor:
else:
logging.error(f"Not standard ID {id_}.")
def get_shape(self, id_: str) -> Optional[Shape]:
def get_shape(self, id_: str) -> Shape:
"""
Get shape or None if there is no shape with such identifier.
@ -262,60 +257,11 @@ class ShapeSpecification:
shape: Shape
color: Color = DEFAULT_COLOR
offset: np.array = np.array((0, 0))
offset: np.ndarray = np.array((0, 0))
flip_horizontally: bool = False
flip_vertically: bool = False
use_outline: bool = True
@classmethod
def from_structure(
cls,
structure: Any,
extractor: ShapeExtractor,
scheme,
color: Color = DEFAULT_COLOR,
) -> "ShapeSpecification":
"""
Parse shape specification from structure, that is just shape string
identifier or dictionary with keys: shape (required), color (optional),
and offset (optional).
"""
shape: Shape = extractor.get_shape(DEFAULT_SHAPE_ID)
color: Color = color
offset: np.array = np.array((0, 0))
flip_horizontally: bool = False
flip_vertically: bool = False
use_outline: bool = True
if isinstance(structure, str):
shape = extractor.get_shape(structure)
elif isinstance(structure, dict):
if "shape" in structure:
shape = extractor.get_shape(structure["shape"])
else:
logging.error(
"Invalid shape specification: `shape` key expected."
)
if "color" in structure:
color = scheme.get_color(structure["color"])
if "offset" in structure:
offset = np.array(structure["offset"])
if "flip_horizontally" in structure:
flip_horizontally = structure["flip_horizontally"]
if "flip_vertically" in structure:
flip_vertically = structure["flip_vertically"]
if "outline" in structure:
use_outline = structure["outline"]
return cls(
shape,
color,
offset,
flip_horizontally,
flip_vertically,
use_outline,
)
def is_default(self) -> bool:
"""Check whether shape is default."""
return self.shape.id_ == DEFAULT_SHAPE_ID
@ -323,7 +269,7 @@ class ShapeSpecification:
def draw(
self,
svg: Drawing,
point: np.array,
point: np.ndarray,
tags: dict[str, Any] = None,
outline: bool = False,
outline_opacity: float = 1.0,
@ -337,14 +283,14 @@ class ShapeSpecification:
:param outline: draw outline for the shape
:param outline_opacity: opacity of the outline
"""
scale: np.array = np.array((1, 1))
scale: np.ndarray = np.array((1, 1))
if self.flip_vertically:
scale = np.array((1, -1))
if self.flip_horizontally:
scale = np.array((-1, 1))
point = np.array(list(map(int, point)))
path = self.shape.get_path(point, self.offset, scale)
point: np.ndarray = np.array(list(map(int, point)))
path: SVGPath = self.shape.get_path(point, self.offset, scale)
path.update({"fill": self.color.hex})
if outline and self.use_outline:
@ -398,7 +344,7 @@ class Icon:
def draw(
self,
svg: svgwrite.Drawing,
point: np.array,
point: np.ndarray,
tags: dict[str, Any] = None,
outline: bool = False,
) -> None:
@ -427,7 +373,7 @@ class Icon:
color: Optional[Color] = None,
outline: bool = False,
outline_opacity: float = 1.0,
):
) -> None:
"""
Draw icon to the SVG file.
@ -442,13 +388,16 @@ class Icon:
if color:
shape_specification.color = color
shape_specification.draw(
svg, (8, 8), outline=outline, outline_opacity=outline_opacity
svg,
np.array((8, 8)),
outline=outline,
outline_opacity=outline_opacity,
)
for shape_specification in self.shape_specifications:
if color:
shape_specification.color = color
shape_specification.draw(svg, (8, 8))
shape_specification.draw(svg, np.array((8, 8)))
with file_name.open("w") as output_file:
svg.write(output_file)

View file

@ -116,7 +116,7 @@ class MapCSSWriter:
if opacity is not None:
elements["icon-opacity"] = f"{opacity:.2f}"
style = matcher.get_style()
style: dict[str, str] = matcher.get_style()
if style:
if "fill" in style:
elements["fill-color"] = style["fill"]

View file

@ -4,7 +4,7 @@ Simple OpenStreetMap renderer.
import argparse
import logging
from pathlib import Path
from typing import Any, Iterator
from typing import Any, Iterator, Optional
import numpy as np
import svgwrite
@ -15,13 +15,13 @@ from svgwrite.shapes import Rect
from roentgen.boundary_box import BoundaryBox
from roentgen.constructor import Constructor
from roentgen.figure import Road
from roentgen.figure import Road, StyledFigure
from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor
from roentgen.map_configuration import LabelMode, MapConfiguration
from roentgen.osm_getter import NetworkError, get_osm
from roentgen.osm_reader import OSMData, OSMNode, OSMReader, OverpassReader
from roentgen.point import Occupied
from roentgen.point import Occupied, Point
from roentgen.road import Intersection, RoadPart
from roentgen.scheme import Scheme
from roentgen.ui import BuildingMode, progress_bar
@ -57,13 +57,15 @@ class Map:
self.svg.add(
Rect((0, 0), self.flinger.size, fill=self.background_color)
)
ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
ways: list[StyledFigure] = sorted(
constructor.figures, key=lambda x: x.line_style.priority
)
ways_length: int = len(ways)
for index, way in enumerate(ways):
progress_bar(index, ways_length, step=10, text="Drawing ways")
path_commands: str = way.get_path(self.flinger)
if path_commands:
path = SVGPath(d=path_commands)
path: SVGPath = SVGPath(d=path_commands)
path.update(way.line_style.style)
self.svg.add(path)
progress_bar(-1, 0, text="Drawing ways")
@ -86,6 +88,7 @@ class Map:
# All other points
occupied: Optional[Occupied]
if self.configuration.overlap == 0:
occupied = None
else:
@ -95,7 +98,9 @@ class Map:
self.configuration.overlap,
)
nodes = sorted(constructor.points, key=lambda x: -x.priority)
nodes: list[Point] = sorted(
constructor.points, key=lambda x: -x.priority
)
steps: int = len(nodes)
for index, node in enumerate(nodes):
@ -158,13 +163,14 @@ class Map:
) -> None:
"""Draw road as simple SVG path."""
self.flinger.get_scale()
width: float
if road.width is not None:
width = road.width
else:
width = road.matcher.default_width
scale = self.flinger.get_scale(road.outers[0][0].coordinates)
scale: float = self.flinger.get_scale(road.outers[0][0].coordinates)
path_commands: str = road.get_path(self.flinger)
path = SVGPath(d=path_commands)
path: SVGPath = SVGPath(d=path_commands)
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
@ -183,8 +189,8 @@ class Map:
for index in range(len(road.outers[0]) - 1):
node_1: OSMNode = road.outers[0][index]
node_2: OSMNode = road.outers[0][index + 1]
point_1: np.array = self.flinger.fling(node_1.coordinates)
point_2: np.array = self.flinger.fling(node_2.coordinates)
point_1: np.ndarray = self.flinger.fling(node_1.coordinates)
point_2: np.ndarray = self.flinger.fling(node_2.coordinates)
scale: float = self.flinger.get_scale(node_1.coordinates)
part_1: RoadPart = RoadPart(point_1, point_2, road.lanes, scale)
part_2: RoadPart = RoadPart(point_2, point_1, road.lanes, scale)
@ -198,7 +204,7 @@ class Map:
nodes[node_2].add(part_2)
for node in nodes:
parts = nodes[node]
parts: set[RoadPart] = nodes[node]
if len(parts) < 4:
continue
intersection: Intersection = Intersection(list(parts))
@ -239,8 +245,8 @@ def ui(options: argparse.Namespace) -> None:
input_file_names = [cache_file_path]
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
min_: np.array
max_: np.array
min_: np.ndarray
max_: np.ndarray
osm_data: OSMData
view_box: BoundaryBox
@ -268,7 +274,7 @@ def ui(options: argparse.Namespace) -> None:
view_box = osm_data.view_box
flinger: Flinger = Flinger(view_box, options.scale)
size: np.array = flinger.size
size: np.ndarray = flinger.size
svg: svgwrite.Drawing = svgwrite.Drawing(
options.output_file_name, size=size
@ -281,7 +287,7 @@ def ui(options: argparse.Namespace) -> None:
osm_data=osm_data,
flinger=flinger,
scheme=scheme,
icon_extractor=icon_extractor,
extractor=icon_extractor,
configuration=configuration,
)
constructor.construct()

View file

@ -68,7 +68,7 @@ class ArgumentParser(argparse.ArgumentParser):
Return Moire table with "Option" and "Description" columns filled with
arguments.
"""
table = [[["Option"], ["Description"]]]
table: Code = [[["Option"], ["Description"]]]
for option in self.arguments:
if option["arguments"][0] == "-h":

View file

@ -8,6 +8,7 @@ from datetime import datetime
from pathlib import Path
from typing import Any, Optional
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
import numpy as np
@ -51,8 +52,7 @@ class Tagged:
"""
def __init__(self, tags: dict[str, str] = None) -> None:
self.tags: dict[str, str]
self.tags = {} if tags is None else tags
self.tags: dict[str, str] = {} if tags is None else tags
def get_tag(self, key: str) -> Optional[str]:
"""
@ -87,7 +87,7 @@ class Tagged:
(KILOMETERS_PATTERN, 1000.0),
(MILES_PATTERN, 1609.344),
]:
matcher = pattern.match(value)
matcher: re.Match = pattern.match(value)
if matcher:
float_value: float = parse_float(matcher.group("value"))
if float_value is not None:
@ -116,7 +116,9 @@ class OSMNode(Tagged):
self.uid: Optional[str] = None
@classmethod
def from_xml_structure(cls, element, is_full: bool = False) -> "OSMNode":
def from_xml_structure(
cls, element: Element, is_full: bool = False
) -> "OSMNode":
"""Parse node from OSM XML `<node>` element."""
node: "OSMNode" = cls()
attributes = element.attrib
@ -174,7 +176,9 @@ class OSMWay(Tagged):
self.uid: Optional[str] = None
@classmethod
def from_xml_structure(cls, element, nodes, is_full: bool) -> "OSMWay":
def from_xml_structure(
cls, element: Element, nodes: dict[int, OSMNode], is_full: bool
) -> "OSMWay":
"""Parse way from OSM XML `<way>` element."""
way = cls(int(element.attrib["id"]))
if is_full:
@ -193,7 +197,7 @@ class OSMWay(Tagged):
return way
def parse_from_structure(
self, structure: dict[str, Any], nodes
self, structure: dict[str, Any], nodes: dict[int, OSMNode]
) -> "OSMWay":
"""
Parse way from Overpass-like structure.
@ -245,7 +249,9 @@ class OSMRelation(Tagged):
self.timestamp: Optional[datetime] = None
@classmethod
def from_xml_structure(cls, element, is_full: bool) -> "OSMRelation":
def from_xml_structure(
cls, element: Element, is_full: bool
) -> "OSMRelation":
"""Parse relation from OSM XML `<relation>` element."""
attributes = element.attrib
relation = cls(int(attributes["id"]))
@ -412,11 +418,11 @@ class OSMReader:
"""
return self.parse_osm(ElementTree.fromstring(text))
def parse_osm(self, root) -> OSMData:
def parse_osm(self, root: Element) -> OSMData:
"""
Parse OSM XML data.
:param root: root of XML data
:param root: top element of XML data
:return: parsed map
"""
for element in root:
@ -437,7 +443,7 @@ class OSMReader:
)
return self.osm_data
def parse_bounds(self, element) -> None:
def parse_bounds(self, element: Element) -> None:
"""Parse view box from XML element."""
attributes = element.attrib
self.osm_data.view_box = BoundaryBox(

View file

@ -24,19 +24,19 @@ class Occupied:
texts, shapes).
"""
def __init__(self, width: int, height: int, overlap: float) -> None:
def __init__(self, width: int, height: int, overlap: int) -> None:
self.matrix = np.full((int(width), int(height)), False, dtype=bool)
self.width: float = width
self.height: float = height
self.overlap: float = overlap
self.overlap: int = overlap
def check(self, point: np.array) -> bool:
def check(self, point: np.ndarray) -> bool:
"""Check whether point is already occupied by other elements."""
if 0 <= point[0] < self.width and 0 <= point[1] < self.height:
return self.matrix[point[0], point[1]]
return True
def register(self, point) -> None:
def register(self, point: np.ndarray) -> None:
"""Register that point is occupied by an element."""
if 0 <= point[0] < self.width and 0 <= point[1] < self.height:
self.matrix[point[0], point[1]] = True
@ -56,8 +56,7 @@ class Point(Tagged):
labels: list[Label],
tags: dict[str, str],
processed: set[str],
point: np.array,
coordinates: np.array,
point: np.ndarray,
priority: float = 0,
is_for_node: bool = True,
draw_outline: bool = True,
@ -70,8 +69,7 @@ class Point(Tagged):
self.labels: list[Label] = labels
self.tags: dict[str, str] = tags
self.processed: set[str] = processed
self.point: np.array = point
self.coordinates: np.array = coordinates
self.point: np.ndarray = point
self.priority: float = priority
self.layer: float = 0
self.is_for_node: bool = is_for_node
@ -110,9 +108,10 @@ class Point(Tagged):
if occupied:
left: float = -(len(self.icon_set.extra_icons) - 1) * 8
for _ in self.icon_set.extra_icons:
if occupied.check(
point: np.ndarray = np.array(
(int(self.point[0] + left), int(self.point[1] + self.y))
):
)
if occupied.check(point):
is_place_for_extra = False
break
left += 16
@ -120,7 +119,7 @@ class Point(Tagged):
if is_place_for_extra:
left: float = -(len(self.icon_set.extra_icons) - 1) * 8
for icon in self.icon_set.extra_icons:
point: np.array = self.point + np.array((left, self.y))
point: np.ndarray = self.point + np.array((left, self.y))
self.draw_point_shape(svg, icon, point, occupied=occupied)
left += 16
if self.icon_set.extra_icons:
@ -130,13 +129,13 @@ class Point(Tagged):
self,
svg: svgwrite.Drawing,
icon: Icon,
position,
occupied,
position: np.ndarray,
occupied: Occupied,
tags: Optional[dict[str, str]] = None,
) -> bool:
"""Draw one combined icon and its outline."""
# Down-cast floats to integers to make icons pixel-perfect.
position = list(map(int, position))
position: np.ndarray = np.array((int(position[0]), int(position[1])))
if occupied and occupied.check(position):
return False
@ -188,11 +187,11 @@ class Point(Tagged):
self,
svg: svgwrite.Drawing,
text: str,
point,
point: np.ndarray,
occupied: Optional[Occupied],
fill: Color,
size: float = 10.0,
out_fill=Color("white"),
out_fill: Color = Color("white"),
out_opacity: float = 0.5,
out_fill_2: Optional[Color] = None,
out_opacity_2: float = 1.0,
@ -207,12 +206,15 @@ class Point(Tagged):
#------#
######
"""
length = len(text) * 6
length: int = len(text) * 6 # FIXME
if occupied:
is_occupied: bool = False
for i in range(-int(length / 2), int(length / 2)):
if occupied.check((int(point[0] + i), int(point[1] - 4))):
text_position: np.ndarray = np.array(
(int(point[0] + i), int(point[1] - 4))
)
if occupied.check(text_position):
is_occupied = True
break
@ -249,7 +251,7 @@ class Point(Tagged):
self.y += 11
def get_size(self) -> np.array:
def get_size(self) -> np.ndarray:
"""
Get width and height of the point visual representation if there is
space for all elements.

View file

@ -6,9 +6,8 @@ from typing import Optional
import numpy as np
import svgwrite
from svgwrite.path import Path
from roentgen.flinger import Flinger
from roentgen.osm_reader import OSMNode
from roentgen.vector import Line, compute_angle, norm, turn_by_angle
__author__ = "Sergey Vartanov"
@ -33,7 +32,7 @@ class Lane:
"""If true, lane is forward, otherwise it's backward."""
self.is_forward = is_forward
def get_width(self, scale: float):
def get_width(self, scale: float) -> float:
"""Get lane width. We use standard 3.7 m lane."""
if self.width is None:
return 3.7 * scale
@ -47,18 +46,18 @@ class RoadPart:
def __init__(
self,
point_1: np.array,
point_2: np.array,
point_1: np.ndarray,
point_2: np.ndarray,
lanes: list[Lane],
scale: False,
scale: float,
) -> None:
"""
:param point_1: start point of the road part
:param point_2: end point of the road part
:param lanes: lane specification
"""
self.point_1: np.array = point_1
self.point_2: np.array = point_2
self.point_1: np.ndarray = point_1
self.point_2: np.ndarray = point_2
self.lanes: list[Lane] = lanes
if lanes:
self.width = sum(map(lambda x: x.get_width(scale), lanes))
@ -67,35 +66,21 @@ class RoadPart:
self.left_offset: float = self.width / 2
self.right_offset: float = self.width / 2
self.turned: np.array = norm(
self.turned: np.ndarray = norm(
turn_by_angle(self.point_2 - self.point_1, np.pi / 2)
)
self.right_vector: np.array = self.turned * self.right_offset
self.left_vector: np.array = -self.turned * self.left_offset
self.right_vector: np.ndarray = self.turned * self.right_offset
self.left_vector: np.ndarray = -self.turned * self.left_offset
self.right_connection: np.array = None
self.left_connection: np.array = None
self.right_projection: np.array = None
self.left_projection: np.array = None
self.right_connection: Optional[np.ndarray] = None
self.left_connection: Optional[np.ndarray] = None
self.right_projection: Optional[np.ndarray] = None
self.left_projection: Optional[np.ndarray] = None
self.left_outer = None
self.right_outer = None
self.point_a = None
self.point_middle = None
@classmethod
def from_nodes(
cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, road, scale
) -> "RoadPart":
"""Construct road part from OSM nodes."""
lanes = [Lane(road.width / road.lanes)] * road.lanes
return cls(
flinger.fling(node_1.coordinates),
flinger.fling(node_2.coordinates),
lanes,
scale,
)
self.left_outer: Optional[np.ndarray] = None
self.right_outer: Optional[np.ndarray] = None
self.point_a: Optional[np.ndarray] = None
self.point_middle: Optional[np.ndarray] = None
def update(self) -> None:
"""Compute additional points."""
@ -136,9 +121,9 @@ class RoadPart:
"""Get an angle between line and x axis."""
return compute_angle(self.point_2 - self.point_1)
def draw_normal(self, drawing: svgwrite.Drawing):
def draw_normal(self, drawing: svgwrite.Drawing) -> None:
"""Draw some debug lines."""
line = drawing.path(
line: Path = drawing.path(
("M", self.point_1, "L", self.point_2),
fill="none",
stroke="#8888FF",
@ -146,15 +131,15 @@ class RoadPart:
)
drawing.add(line)
def draw_debug(self, drawing: svgwrite.Drawing):
def draw_debug(self, drawing: svgwrite.Drawing) -> None:
"""Draw some debug lines."""
line = drawing.path(
line: Path = drawing.path(
("M", self.point_1, "L", self.point_2),
fill="none",
stroke="#000000",
)
drawing.add(line)
line = drawing.path(
line: Path = drawing.path(
(
"M", self.point_1 + self.right_vector,
"L", self.point_2 + self.right_vector,
@ -227,7 +212,7 @@ class RoadPart:
# self.draw_entrance(drawing, True)
def draw(self, drawing: svgwrite.Drawing):
def draw(self, drawing: svgwrite.Drawing) -> None:
"""Draw road part."""
if self.left_connection is not None:
path_commands = [
@ -239,7 +224,9 @@ class RoadPart:
] # fmt: skip
drawing.add(drawing.path(path_commands, fill="#CCCCCC"))
def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool = False):
def draw_entrance(
self, drawing: svgwrite.Drawing, is_debug: bool = False
) -> None:
"""Draw intersection entrance part."""
if (
self.left_connection is not None
@ -263,7 +250,7 @@ class RoadPart:
else:
drawing.add(drawing.path(path_commands, fill="#88FF88"))
def draw_lanes(self, drawing: svgwrite.Drawing, scale: float):
def draw_lanes(self, drawing: svgwrite.Drawing, scale: float) -> None:
"""Draw lane delimiters."""
for lane in self.lanes:
shift = self.right_vector - self.turned * lane.get_width(scale)
@ -298,7 +285,7 @@ class Intersection:
part_2.point_1 + part_2.left_vector,
part_2.point_2 + part_2.left_vector,
)
intersection: np.array = line_1.get_intersection_point(line_2)
intersection: np.ndarray = line_1.get_intersection_point(line_2)
# if np.linalg.norm(intersection - part_1.point_2) < 300:
part_1.right_connection = intersection
part_2.left_connection = intersection

View file

@ -1,11 +1,13 @@
"""
Röntgen drawing scheme.
"""
import logging
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Optional, Union
import numpy as np
import yaml
from colour import Color
@ -15,6 +17,7 @@ from roentgen.icon import (
DEFAULT_SHAPE_ID,
Icon,
IconSet,
Shape,
ShapeExtractor,
ShapeSpecification,
)
@ -23,6 +26,8 @@ from roentgen.text import Label, get_address, get_text
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
IconDescription = list[Union[str, dict[str, str]]]
@dataclass
class LineStyle:
@ -170,27 +175,27 @@ class NodeMatcher(Matcher):
if "draw" in structure:
self.draw = structure["draw"]
self.shapes = None
self.shapes: Optional[IconDescription] = None
if "shapes" in structure:
self.shapes = structure["shapes"]
self.over_icon = None
self.over_icon: Optional[IconDescription] = None
if "over_icon" in structure:
self.over_icon = structure["over_icon"]
self.add_shapes = None
self.add_shapes: Optional[IconDescription] = None
if "add_shapes" in structure:
self.add_shapes = structure["add_shapes"]
self.set_main_color = None
self.set_main_color: Optional[str] = None
if "set_main_color" in structure:
self.set_main_color = structure["set_main_color"]
self.under_icon = None
self.under_icon: Optional[IconDescription] = None
if "under_icon" in structure:
self.under_icon = structure["under_icon"]
self.with_icon = None
self.with_icon: Optional[IconDescription] = None
if "with_icon" in structure:
self.with_icon = structure["with_icon"]
@ -369,23 +374,21 @@ class Scheme:
processed |= matcher_tags
if matcher.shapes:
specifications = [
ShapeSpecification.from_structure(x, extractor, self)
self.get_shape_specification(x, extractor)
for x in matcher.shapes
]
main_icon = Icon(specifications)
processed |= matcher_tags
if matcher.over_icon and main_icon:
specifications = [
ShapeSpecification.from_structure(x, extractor, self)
self.get_shape_specification(x, extractor)
for x in matcher.over_icon
]
main_icon.add_specifications(specifications)
processed |= matcher_tags
if matcher.add_shapes:
specifications = [
ShapeSpecification.from_structure(
x, extractor, self, Color("#888888")
)
self.get_shape_specification(x, extractor, Color("#888888"))
for x in matcher.add_shapes
]
extra_icons += [Icon(specifications)]
@ -436,7 +439,7 @@ class Scheme:
return returned, priority
def get_style(self, tags: dict[str, Any]):
def get_style(self, tags: dict[str, Any]) -> list[LineStyle]:
"""Get line style based on tags and scale."""
line_styles = []
@ -548,3 +551,50 @@ class Scheme:
:param processed: processed set
"""
[processed.add(tag) for tag in tags if self.is_no_drawable(tag)]
def get_shape_specification(
self,
structure: Union[str, dict[str, Any]],
extractor: ShapeExtractor,
color: Color = DEFAULT_COLOR,
) -> ShapeSpecification:
"""
Parse shape specification from structure, that is just shape string
identifier or dictionary with keys: shape (required), color (optional),
and offset (optional).
"""
shape: Shape = extractor.get_shape(DEFAULT_SHAPE_ID)
color: Color = color
offset: np.ndarray = np.array((0, 0))
flip_horizontally: bool = False
flip_vertically: bool = False
use_outline: bool = True
if isinstance(structure, str):
shape = extractor.get_shape(structure)
elif isinstance(structure, dict):
if "shape" in structure:
shape = extractor.get_shape(structure["shape"])
else:
logging.error(
"Invalid shape specification: `shape` key expected."
)
if "color" in structure:
color = self.get_color(structure["color"])
if "offset" in structure:
offset = np.array(structure["offset"])
if "flip_horizontally" in structure:
flip_horizontally = structure["flip_horizontally"]
if "flip_vertically" in structure:
flip_vertically = structure["flip_vertically"]
if "outline" in structure:
use_outline = structure["outline"]
return ShapeSpecification(
shape,
color,
offset,
flip_horizontally,
flip_vertically,
use_outline,
)

View file

@ -26,7 +26,10 @@ class _Handler(SimpleHTTPRequestHandler):
options = None
def __init__(
self, request: bytes, client_address: tuple[str, int], server
self,
request: bytes,
client_address: tuple[str, int],
server: HTTPServer,
) -> None:
super().__init__(request, client_address, server)

View file

@ -44,7 +44,7 @@ class Tile:
scale: int
@classmethod
def from_coordinates(cls, coordinates: np.array, scale: int):
def from_coordinates(cls, coordinates: np.ndarray, scale: int) -> "Tile":
"""
Code from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
@ -57,7 +57,7 @@ class Tile:
y: int = int((1.0 - np.arcsinh(np.tan(lat_rad)) / np.pi) / 2.0 * n)
return cls(x, y, scale)
def get_coordinates(self) -> np.array:
def get_coordinates(self) -> np.ndarray:
"""
Return geo coordinates of the north-west corner of the tile.
@ -69,7 +69,7 @@ class Tile:
lat_deg: np.ndarray = np.degrees(lat_rad)
return np.array((lat_deg, lon_deg))
def get_boundary_box(self) -> tuple[np.array, np.array]:
def get_boundary_box(self) -> tuple[np.ndarray, np.ndarray]:
"""
Get geographical boundary box of the tile: north-west and south-east
points.
@ -81,8 +81,8 @@ class Tile:
def get_extended_boundary_box(self) -> BoundaryBox:
"""Same as get_boundary_box, but with extended boundaries."""
point_1: np.array = self.get_coordinates()
point_2: np.array = Tile(
point_1: np.ndarray = self.get_coordinates()
point_2: np.ndarray = Tile(
self.x + 1, self.y + 1, self.scale
).get_coordinates()
@ -152,7 +152,7 @@ class Tile:
flinger: Flinger = Flinger(
BoundaryBox(left, bottom, right, top), self.scale
)
size: np.array = flinger.size
size: np.ndarray = flinger.size
output_file_name: Path = self.get_file_name(directory_name)
@ -196,7 +196,9 @@ class Tiles:
boundary_box: BoundaryBox
@classmethod
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
def from_boundary_box(
cls, boundary_box: BoundaryBox, scale: int
) -> "Tiles":
"""
Create minimal set of tiles that cover boundary box.

View file

@ -14,7 +14,7 @@ BOXES: str = " ▏▎▍▌▋▊▉"
BOXES_LENGTH: int = len(BOXES)
def parse_options(args) -> argparse.Namespace:
def parse_options(args: argparse.Namespace) -> argparse.Namespace:
"""Parse Röntgen command-line options."""
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="Röntgen. OpenStreetMap renderer with custom icon set"

View file

@ -7,7 +7,7 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
def compute_angle(vector: np.array):
def compute_angle(vector: np.ndarray) -> float:
"""
For the given vector compute an angle between it and (1, 0) vector. The
result is in [0, 2π].
@ -23,7 +23,7 @@ def compute_angle(vector: np.array):
return np.arctan(vector[1] / vector[0])
def turn_by_angle(vector: np.array, angle: float):
def turn_by_angle(vector: np.ndarray, angle: float) -> np.ndarray:
"""Turn vector by an angle."""
return np.array(
(
@ -33,7 +33,7 @@ def turn_by_angle(vector: np.array, angle: float):
)
def norm(vector: np.array) -> np.array:
def norm(vector: np.ndarray) -> np.ndarray:
"""Compute vector with the same direction and length 1."""
return vector / np.linalg.norm(vector)
@ -41,26 +41,26 @@ def norm(vector: np.array) -> np.array:
class Line:
"""Infinity line: Ax + By + C = 0."""
def __init__(self, start: np.array, end: np.array) -> None:
def __init__(self, start: np.ndarray, end: np.ndarray) -> None:
# if start.near(end):
# util.error("cannot create line by one point")
self.a: float = start[1] - end[1]
self.b: float = end[0] - start[0]
self.c: float = start[0] * end[1] - end[0] * start[1]
def parallel_shift(self, shift: np.array):
def parallel_shift(self, shift: np.ndarray) -> None:
"""
Shift current vector according with shift.
:param shift: shift vector
"""
self.c -= self.a * shift.x + self.b * shift.y
self.c -= self.a * shift[0] + self.b * shift[1]
def is_parallel(self, other: "Line") -> bool:
"""If lines are parallel or equal."""
return np.allclose(other.a * self.b - self.a * other.b, 0)
def get_intersection_point(self, other: "Line") -> np.array:
def get_intersection_point(self, other: "Line") -> np.ndarray:
"""Get point of intersection current line with other."""
if other.a * self.b - self.a * other.b == 0:
return np.array((0, 0))

View file

@ -17,17 +17,17 @@ def init_collection() -> IconCollection:
return IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR)
def test_grid(init_collection) -> None:
def test_grid(init_collection: IconCollection) -> None:
"""Test grid drawing."""
init_collection.draw_grid(workspace.output_path / "grid.svg")
def test_icons_by_id(init_collection) -> None:
def test_icons_by_id(init_collection: IconCollection) -> None:
"""Test individual icons drawing."""
init_collection.draw_icons(workspace.get_icons_by_id_path(), by_name=False)
def test_icons_by_name(init_collection) -> None:
def test_icons_by_name(init_collection: IconCollection) -> None:
"""Test drawing individual icons that have names."""
init_collection.draw_icons(workspace.get_icons_by_name_path(), by_name=True)

View file

@ -8,7 +8,7 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
def construct_labels(tags) -> list[Label]:
def construct_labels(tags: dict[str, str]) -> list[Label]:
"""Construct labels from OSM node tags."""
processed: set[str] = set()
return SCHEME.construct_text(tags, "all", processed)