mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-24 14:36:25 +02:00
Refactor type annotaions; get rid of minidom.
This commit is contained in:
parent
81502e542e
commit
cdeffc8758
22 changed files with 401 additions and 353 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
139
roentgen/icon.py
139
roentgen/icon.py
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue