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

View file

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

View file

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

View file

@ -9,6 +9,8 @@ import cairo
import numpy as np import numpy as np
import svgwrite import svgwrite
from colour import Color from colour import Color
from cairo import Context, ImageSurface
from svgwrite.base import BaseElement
from svgwrite.path import Path as SVGPath from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect from svgwrite.shapes import Rect
from svgwrite.text import Text from svgwrite.text import Text
@ -29,7 +31,7 @@ class Style:
stroke: Optional[Color] = None stroke: Optional[Color] = None
width: float = 1 width: float = 1
def update_svg_element(self, element) -> None: def update_svg_element(self, element: BaseElement) -> None:
"""Set style for SVG element.""" """Set style for SVG element."""
if self.fill is not None: if self.fill is not None:
element.update({"fill": self.fill}) element.update({"fill": self.fill})
@ -38,14 +40,14 @@ class Style:
if self.stroke is not None: if self.stroke is not None:
element.update({"stroke": self.stroke, "stroke-width": self.width}) 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.""" """Set style for context and draw fill."""
context.set_source_rgba( context.set_source_rgba(
self.fill.get_red(), self.fill.get_green(), self.fill.get_blue(), 1 self.fill.get_red(), self.fill.get_green(), self.fill.get_blue(), 1
) )
context.fill() context.fill()
def draw_png_stroke(self, context) -> None: def draw_png_stroke(self, context: Context) -> None:
"""Set style for context and draw stroke.""" """Set style for context and draw stroke."""
context.set_source_rgba( context.set_source_rgba(
self.stroke.get_red(), self.stroke.get_red(),
@ -81,7 +83,9 @@ class Drawing:
"""Draw path.""" """Draw path."""
raise NotImplementedError 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.""" """Draw text."""
raise NotImplementedError raise NotImplementedError
@ -97,7 +101,9 @@ class SVGDrawing(Drawing):
def __init__(self, file_path: Path, width: int, height: int) -> None: def __init__(self, file_path: Path, width: int, height: int) -> None:
super().__init__(file_path, width, height) 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( def rectangle(
self, point_1: np.ndarray, point_2: np.ndarray, style: Style 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: def path(self, commands: PathCommands, style: Style) -> None:
"""Draw path.""" """Draw path."""
path = SVGPath(d=commands) path: SVGPath = SVGPath(d=commands)
style.update_svg_element(path) style.update_svg_element(path)
self.image.add(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.""" """Draw text."""
self.image.add( self.image.add(
Text(text, (float(point[0]), float(point[1])), fill=color) 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: def __init__(self, file_path: Path, width: int, height: int) -> None:
super().__init__(file_path, width, height) super().__init__(file_path, width, height)
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self.surface: ImageSurface = ImageSurface(
self.context = cairo.Context(self.surface) cairo.FORMAT_ARGB32, width, height
)
self.context: Context = Context(self.surface)
def rectangle( def rectangle(
self, point_1: np.ndarray, point_2: np.ndarray, style: Style self, point_1: np.ndarray, point_2: np.ndarray, style: Style
@ -181,7 +191,7 @@ class PNGDrawing(Drawing):
index: int = 0 index: int = 0
while index < len(commands): while index < len(commands):
element = commands[index] element: Union[float, str, np.ndarray] = commands[index]
if isinstance(element, str): if isinstance(element, str):
is_absolute: bool = element.lower() != element is_absolute: bool = element.lower() != element
@ -192,10 +202,11 @@ class PNGDrawing(Drawing):
start_point = None start_point = None
elif command in "ml": elif command in "ml":
point: np.ndarray
if is_absolute: if is_absolute:
point: np.ndarray = commands[index] point = commands[index]
else: else:
point: np.ndarray = current + commands[index] point = current + commands[index]
current = point current = point
if command == "m": if command == "m":
self.context.move_to(point[0], point[1]) self.context.move_to(point[0], point[1])
@ -225,6 +236,7 @@ class PNGDrawing(Drawing):
elif command in "vh": elif command in "vh":
assert isinstance(commands[index], float) assert isinstance(commands[index], float)
point: np.ndarray
if is_absolute: if is_absolute:
if command == "v": if command == "v":
point = np.array((0, commands[index])) point = np.array((0, commands[index]))
@ -254,7 +266,9 @@ class PNGDrawing(Drawing):
self._do_path(commands) self._do_path(commands)
style.draw_png_stroke(self.context) 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.""" """Draw text."""
self.context.set_source_rgb( self.context.set_source_rgb(
color.get_red(), color.get_green(), color.get_blue() color.get_red(), color.get_green(), color.get_blue()
@ -282,7 +296,7 @@ def parse_path(path: str) -> PathCommands:
result.append(float(part)) result.append(float(part))
else: else:
if "," in part: if "," in part:
elements = part.split(",") elements: list[str] = part.split(",")
result.append(np.array(list(map(float, elements)))) result.append(np.array(list(map(float, elements))))
else: else:
result.append(np.array((float(part), float(parts[index + 1])))) 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 numpy as np
import svgwrite import svgwrite
from svgwrite.path import Path as SVGPath
from roentgen.icon import ShapeExtractor from roentgen.icon import ShapeExtractor
from roentgen.point import Point from roentgen.point import Point
from roentgen.scheme import LineStyle, Scheme from roentgen.scheme import LineStyle, Scheme
from roentgen.text import Label
from roentgen.workspace import workspace from roentgen.workspace import workspace
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -19,12 +21,17 @@ __email__ = "me@enzet.ru"
def draw_element(options: argparse.Namespace) -> None: def draw_element(options: argparse.Namespace) -> None:
"""Draw single node, line, or area.""" """Draw single node, line, or area."""
target: str
tags_description: str
if options.node: if options.node:
target: str = "node" target = "node"
tags_description = options.node tags_description = options.node
elif options.way:
target = "way"
tags_description = options.way
else: else:
# Not implemented yet. target = "area"
exit(1) tags_description = options.area
tags: dict[str, str] = dict( tags: dict[str, str] = dict(
[x.split("=") for x in tags_description.split(",")] [x.split("=") for x in tags_description.split(",")]
@ -36,26 +43,27 @@ def draw_element(options: argparse.Namespace) -> None:
processed: set[str] = set() processed: set[str] = set()
icon, priority = scheme.get_icon(extractor, tags, processed) icon, priority = scheme.get_icon(extractor, tags, processed)
is_for_node: bool = target == "node" is_for_node: bool = target == "node"
labels = scheme.construct_text(tags, "all", processed) labels: list[Label] = scheme.construct_text(tags, "all", processed)
point = Point( point: Point = Point(
icon, icon,
labels, labels,
tags, tags,
processed, processed,
np.array((32, 32)), np.array((32, 32)),
None,
is_for_node=is_for_node, is_for_node=is_for_node,
draw_outline=is_for_node, draw_outline=is_for_node,
) )
border: np.array = np.array((16, 16)) border: np.ndarray = np.array((16, 16))
size: np.array = point.get_size() + border size: np.ndarray = point.get_size() + border
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2)) point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
output_file_path: Path = workspace.output_path / "element.svg" 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): for style in scheme.get_style(tags):
style: LineStyle 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) path.update(style.style)
svg.add(path) svg.add(path)
point.draw_main_shapes(svg) point.draw_main_shapes(svg)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,13 @@
""" """
Röntgen drawing scheme. Röntgen drawing scheme.
""" """
import logging
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Union from typing import Any, Optional, Union
import numpy as np
import yaml import yaml
from colour import Color from colour import Color
@ -15,6 +17,7 @@ from roentgen.icon import (
DEFAULT_SHAPE_ID, DEFAULT_SHAPE_ID,
Icon, Icon,
IconSet, IconSet,
Shape,
ShapeExtractor, ShapeExtractor,
ShapeSpecification, ShapeSpecification,
) )
@ -23,6 +26,8 @@ from roentgen.text import Label, get_address, get_text
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
IconDescription = list[Union[str, dict[str, str]]]
@dataclass @dataclass
class LineStyle: class LineStyle:
@ -170,27 +175,27 @@ class NodeMatcher(Matcher):
if "draw" in structure: if "draw" in structure:
self.draw = structure["draw"] self.draw = structure["draw"]
self.shapes = None self.shapes: Optional[IconDescription] = None
if "shapes" in structure: if "shapes" in structure:
self.shapes = structure["shapes"] self.shapes = structure["shapes"]
self.over_icon = None self.over_icon: Optional[IconDescription] = None
if "over_icon" in structure: if "over_icon" in structure:
self.over_icon = structure["over_icon"] self.over_icon = structure["over_icon"]
self.add_shapes = None self.add_shapes: Optional[IconDescription] = None
if "add_shapes" in structure: if "add_shapes" in structure:
self.add_shapes = structure["add_shapes"] self.add_shapes = structure["add_shapes"]
self.set_main_color = None self.set_main_color: Optional[str] = None
if "set_main_color" in structure: if "set_main_color" in structure:
self.set_main_color = structure["set_main_color"] self.set_main_color = structure["set_main_color"]
self.under_icon = None self.under_icon: Optional[IconDescription] = None
if "under_icon" in structure: if "under_icon" in structure:
self.under_icon = structure["under_icon"] self.under_icon = structure["under_icon"]
self.with_icon = None self.with_icon: Optional[IconDescription] = None
if "with_icon" in structure: if "with_icon" in structure:
self.with_icon = structure["with_icon"] self.with_icon = structure["with_icon"]
@ -369,23 +374,21 @@ class Scheme:
processed |= matcher_tags processed |= matcher_tags
if matcher.shapes: if matcher.shapes:
specifications = [ specifications = [
ShapeSpecification.from_structure(x, extractor, self) self.get_shape_specification(x, extractor)
for x in matcher.shapes for x in matcher.shapes
] ]
main_icon = Icon(specifications) main_icon = Icon(specifications)
processed |= matcher_tags processed |= matcher_tags
if matcher.over_icon and main_icon: if matcher.over_icon and main_icon:
specifications = [ specifications = [
ShapeSpecification.from_structure(x, extractor, self) self.get_shape_specification(x, extractor)
for x in matcher.over_icon for x in matcher.over_icon
] ]
main_icon.add_specifications(specifications) main_icon.add_specifications(specifications)
processed |= matcher_tags processed |= matcher_tags
if matcher.add_shapes: if matcher.add_shapes:
specifications = [ specifications = [
ShapeSpecification.from_structure( self.get_shape_specification(x, extractor, Color("#888888"))
x, extractor, self, Color("#888888")
)
for x in matcher.add_shapes for x in matcher.add_shapes
] ]
extra_icons += [Icon(specifications)] extra_icons += [Icon(specifications)]
@ -436,7 +439,7 @@ class Scheme:
return returned, priority 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.""" """Get line style based on tags and scale."""
line_styles = [] line_styles = []
@ -548,3 +551,50 @@ class Scheme:
:param processed: processed set :param processed: processed set
""" """
[processed.add(tag) for tag in tags if self.is_no_drawable(tag)] [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 options = None
def __init__( def __init__(
self, request: bytes, client_address: tuple[str, int], server self,
request: bytes,
client_address: tuple[str, int],
server: HTTPServer,
) -> None: ) -> None:
super().__init__(request, client_address, server) super().__init__(request, client_address, server)

View file

@ -44,7 +44,7 @@ class Tile:
scale: int scale: int
@classmethod @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 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) y: int = int((1.0 - np.arcsinh(np.tan(lat_rad)) / np.pi) / 2.0 * n)
return cls(x, y, scale) 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. 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) lat_deg: np.ndarray = np.degrees(lat_rad)
return np.array((lat_deg, lon_deg)) 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 Get geographical boundary box of the tile: north-west and south-east
points. points.
@ -81,8 +81,8 @@ class Tile:
def get_extended_boundary_box(self) -> BoundaryBox: def get_extended_boundary_box(self) -> BoundaryBox:
"""Same as get_boundary_box, but with extended boundaries.""" """Same as get_boundary_box, but with extended boundaries."""
point_1: np.array = self.get_coordinates() point_1: np.ndarray = self.get_coordinates()
point_2: np.array = Tile( point_2: np.ndarray = Tile(
self.x + 1, self.y + 1, self.scale self.x + 1, self.y + 1, self.scale
).get_coordinates() ).get_coordinates()
@ -152,7 +152,7 @@ class Tile:
flinger: Flinger = Flinger( flinger: Flinger = Flinger(
BoundaryBox(left, bottom, right, top), self.scale 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) output_file_name: Path = self.get_file_name(directory_name)
@ -196,7 +196,9 @@ class Tiles:
boundary_box: BoundaryBox boundary_box: BoundaryBox
@classmethod @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. Create minimal set of tiles that cover boundary box.

View file

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

View file

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

View file

@ -17,17 +17,17 @@ def init_collection() -> IconCollection:
return IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR) return IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR)
def test_grid(init_collection) -> None: def test_grid(init_collection: IconCollection) -> None:
"""Test grid drawing.""" """Test grid drawing."""
init_collection.draw_grid(workspace.output_path / "grid.svg") 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.""" """Test individual icons drawing."""
init_collection.draw_icons(workspace.get_icons_by_id_path(), by_name=False) 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.""" """Test drawing individual icons that have names."""
init_collection.draw_icons(workspace.get_icons_by_name_path(), by_name=True) 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" __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.""" """Construct labels from OSM node tags."""
processed: set[str] = set() processed: set[str] = set()
return SCHEME.construct_text(tags, "all", processed) return SCHEME.construct_text(tags, "all", processed)