Refactor figures; get use of Python 3.9 features.

This commit is contained in:
Sergey Vartanov 2021-08-18 08:38:33 +03:00
parent 053324451a
commit 3bcf026862
36 changed files with 698 additions and 750 deletions

View file

@ -18,31 +18,32 @@ from roentgen.grid import draw_icons
from roentgen.icon import ShapeExtractor
from roentgen.mapper import (
AUTHOR_MODE,
CREATION_TIME_MODE,
Painter,
TIME_MODE,
Map,
check_level_number,
check_level_overground,
)
from roentgen.osm_getter import NetworkError, get_osm
from roentgen.osm_reader import Map, OSMReader, OverpassReader
from roentgen.osm_reader import OSMData, OSMReader, OverpassReader
from roentgen.point import Point
from roentgen.scheme import LineStyle, Scheme
from roentgen.ui import BoundaryBox, parse_options
from roentgen.util import MinMax
from roentgen.workspace import workspace
from roentgen.workspace import Workspace
def main(options) -> None:
"""
Röntgen entry point.
:param argv: command-line arguments
:param options: command-line arguments
"""
if not options.boundary_box and not options.input_file_name:
logging.fatal("Specify either --boundary-box, or --input.")
exit(1)
if options.boundary_box:
box: List[float] = list(
map(float, options.boundary_box.replace(" ", "").split(","))
)
boundary_box = BoundaryBox(box[0], box[1], box[2], box[3])
boundary_box: BoundaryBox = BoundaryBox.from_text(options.boundary_box)
cache_path: Path = Path(options.cache)
cache_path.mkdir(parents=True, exist_ok=True)
@ -53,28 +54,35 @@ def main(options) -> None:
input_file_names = list(map(Path, options.input_file_name))
else:
try:
get_osm(boundary_box, cache_path)
cache_file_path: Path = (
cache_path / f"{boundary_box.get_format()}.osm"
)
get_osm(boundary_box, cache_file_path)
except NetworkError as e:
logging.fatal(e.message)
sys.exit(1)
input_file_names = [cache_path / f"{options.boundary_box}.osm"]
input_file_names = [cache_file_path]
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
min_: np.array
max_: np.array
map_: Map
osm_data: OSMData
if input_file_names[0].name.endswith(".json"):
reader: OverpassReader = OverpassReader()
reader.parse_json_file(input_file_names[0])
map_ = reader.map_
osm_data = reader.osm_data
view_box = MinMax(
np.array((map_.boundary_box[0].min_, map_.boundary_box[1].min_)),
np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_)),
np.array(
(osm_data.boundary_box[0].min_, osm_data.boundary_box[1].min_)
),
np.array(
(osm_data.boundary_box[0].max_, osm_data.boundary_box[1].max_)
),
)
else:
is_full: bool = options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE]
osm_reader = OSMReader(is_full=is_full)
for file_name in input_file_names:
@ -84,24 +92,19 @@ def main(options) -> None:
osm_reader.parse_osm_file(file_name)
map_ = osm_reader.map_
osm_data = osm_reader.osm_data
if options.boundary_box:
boundary_box: List[float] = list(
map(float, options.boundary_box.split(","))
)
view_box = MinMax(
np.array((boundary_box[1], boundary_box[0])),
np.array((boundary_box[3], boundary_box[2])),
np.array((boundary_box.bottom, boundary_box.left)),
np.array((boundary_box.top, boundary_box.right)),
)
else:
view_box = map_.view_box
view_box = osm_data.view_box
flinger: Flinger = Flinger(view_box, options.scale)
size: np.array = flinger.size
Path("out").mkdir(parents=True, exist_ok=True)
svg: svgwrite.Drawing = svgwrite.Drawing(
options.output_file_name, size=size
)
@ -131,7 +134,7 @@ def main(options) -> None:
return True
constructor: Constructor = Constructor(
map_,
osm_data,
flinger,
scheme,
icon_extractor,
@ -141,17 +144,14 @@ def main(options) -> None:
)
constructor.construct()
painter: Painter = Painter(
painter: Map = Map(
overlap=options.overlap,
mode=options.mode,
label_mode=options.label_mode,
map_=map_,
flinger=flinger,
svg=svg,
icon_extractor=icon_extractor,
scheme=scheme,
)
painter.draw(constructor)
print(f"Writing output SVG to {options.output_file_name}...")
@ -164,16 +164,18 @@ def draw_element(options):
Draw single node, line, or area.
"""
if options.node:
target = "node"
target: str = "node"
tags_description = options.node
else:
# Not implemented yet.
sys.exit(1)
tags = dict([x.split("=") for x in tags_description.split(",")])
scheme: Scheme = Scheme(Path("scheme/default.yml"))
tags: dict[str, str] = dict(
[x.split("=") for x in tags_description.split(",")]
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
extractor: ShapeExtractor = ShapeExtractor(
Path("icons/icons.svg"), Path("icons/config.json")
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
processed: Set[str] = set()
icon, priority = scheme.get_icon(extractor, tags, processed)
@ -193,8 +195,8 @@ def draw_element(options):
size: np.array = point.get_size() + border
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
Path("out").mkdir(parents=True, exist_ok=True)
svg = svgwrite.Drawing("out/element.svg", size.astype(float))
output_file_path: Path = workspace.output_path / "element.svg"
svg = svgwrite.Drawing(str(output_file_path), size.astype(float))
for style in scheme.get_style(tags, 18):
style: LineStyle
path = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
@ -203,44 +205,47 @@ def draw_element(options):
point.draw_main_shapes(svg)
point.draw_extra_shapes(svg)
point.draw_texts(svg)
svg.write(open("out/element.svg", "w+"))
with output_file_path.open("w+") as output_file:
svg.write(output_file)
logging.info(f"Element is written to {output_file_path}.")
def init_scheme() -> Scheme:
return Scheme(Path("scheme/default.yml"))
return Scheme(workspace.DEFAULT_SCHEME_PATH)
if __name__ == "__main__":
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
workspace: Workspace = Workspace(Path("out"))
options: argparse.Namespace = parse_options(sys.argv)
arguments: argparse.Namespace = parse_options(sys.argv)
if options.command == "render":
main(options)
if arguments.command == "render":
main(arguments)
elif options.command == "tile":
elif arguments.command == "tile":
from roentgen import tile
tile.ui(options)
tile.ui(arguments)
elif options.command == "icons":
elif arguments.command == "icons":
draw_icons()
elif options.command == "mapcss":
elif arguments.command == "mapcss":
from roentgen import mapcss
mapcss.ui(options)
mapcss.ui(arguments)
elif options.command == "element":
draw_element(options)
elif arguments.command == "element":
draw_element(arguments)
elif options.command == "server":
elif arguments.command == "server":
from roentgen import server
server.ui(options)
server.ui(arguments)
elif options.command == "taginfo":
elif arguments.command == "taginfo":
from roentgen.taginfo import write_taginfo_project_file
write_taginfo_project_file(init_scheme())

View file

@ -37,6 +37,7 @@ def get_gradient_color(
range_coefficient: float = (
0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta()
)
# If value is out of range, set it to boundary value.
range_coefficient = min(1.0, max(0.0, range_coefficient))
index: int = int(range_coefficient * color_length)
coefficient: float = (

View file

@ -4,31 +4,33 @@ Construct Röntgen nodes and ways.
import logging
from datetime import datetime
from hashlib import sha256
from typing import Any, Dict, Iterator, List, Optional, Set
from typing import Any, Iterator, Optional, Union
import numpy as np
from colour import Color
from roentgen import ui
from roentgen.color import get_gradient_color
from roentgen.figure import Building, Road, StyledFigure
from roentgen.figure import Building, Road, StyledFigure, Tree, DirectionSector
from roentgen.flinger import Flinger
# fmt: off
from roentgen.icon import (
DEFAULT_SMALL_SHAPE_ID, Icon, IconSet, ShapeExtractor, ShapeSpecification
)
from roentgen.osm_reader import Map, OSMNode, OSMRelation, OSMWay, Tagged
from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay
from roentgen.point import Point
from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme
from roentgen.ui import TIME_MODE, AUTHOR_MODE
from roentgen.util import MinMax
# fmt: on
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
DEBUG: bool = False
TIME_COLOR_SCALE: List[Color] = [
TIME_COLOR_SCALE: list[Color] = [
Color("#581845"),
Color("#900C3F"),
Color("#C70039"),
@ -38,14 +40,14 @@ TIME_COLOR_SCALE: List[Color] = [
]
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
def line_center(nodes: list[OSMNode], flinger: Flinger) -> np.array:
"""
Get geometric center of nodes set.
:param nodes: node list
:param flinger: flinger that remap geo positions
"""
boundary: List[MinMax] = [MinMax(), MinMax()]
boundary: list[MinMax] = [MinMax(), MinMax()]
for node in nodes:
boundary[0].update(node.coordinates[0])
@ -74,14 +76,14 @@ def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color:
return get_gradient_color(time, boundaries, TIME_COLOR_SCALE)
def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
def glue(ways: list[OSMWay]) -> list[list[OSMNode]]:
"""
Try to glue ways that share nodes.
:param ways: ways to glue
"""
result: List[List[OSMNode]] = []
to_process: Set[OSMWay] = set()
result: list[list[OSMNode]] = []
to_process: set[OSMWay] = set()
for way in ways:
if way.is_cycle():
@ -112,9 +114,7 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
def is_cycle(nodes) -> bool:
"""
Is way a cycle way or an area boundary.
"""
"""Is way a cycle way or an area boundary."""
return nodes[0] == nodes[-1]
@ -125,7 +125,7 @@ class Constructor:
def __init__(
self,
map_: Map,
osm_data: OSMData,
flinger: Flinger,
scheme: Scheme,
icon_extractor: ShapeExtractor,
@ -136,59 +136,51 @@ class Constructor:
self.check_level = check_level
self.mode: str = mode
self.seed: str = seed
self.map_: Map = map_
self.osm_data: OSMData = osm_data
self.flinger: Flinger = flinger
self.scheme: Scheme = scheme
self.icon_extractor = icon_extractor
self.points: List[Point] = []
self.figures: List[StyledFigure] = []
self.buildings: List[Building] = []
self.roads: List[Road] = []
self.points: list[Point] = []
self.figures: list[StyledFigure] = []
self.buildings: list[Building] = []
self.roads: list[Road] = []
self.trees: list[Tree] = []
self.direction_sectors: list[DirectionSector] = []
self.heights: Set[float] = {2, 4}
self.heights: set[float] = {2, 4}
def add_building(self, building: Building) -> None:
"""
Add building and update levels.
"""
"""Add building and update levels."""
self.buildings.append(building)
self.heights.add(building.height)
self.heights.add(building.min_height)
def construct(self) -> None:
"""
Construct nodes, ways, and relations.
"""
"""Construct nodes, ways, and relations."""
self.construct_ways()
self.construct_relations()
self.construct_nodes()
def construct_ways(self) -> None:
"""
Construct Röntgen ways.
"""
way_number: int = 0
for way_id in self.map_.ways:
"""Construct Röntgen ways."""
for index, way_id in enumerate(self.osm_data.ways):
ui.progress_bar(
way_number,
len(self.map_.ways),
index,
len(self.osm_data.ways),
step=10,
text="Constructing ways",
)
way_number += 1
way: OSMWay = self.map_.ways[way_id]
if not self.check_level(way.tags):
continue
way: OSMWay = self.osm_data.ways[way_id]
self.construct_line(way, [], [way.nodes])
ui.progress_bar(-1, len(self.map_.ways), text="Constructing ways")
ui.progress_bar(-1, len(self.osm_data.ways), text="Constructing ways")
def construct_line(
self,
line: Optional[Tagged],
inners: List[List[OSMNode]],
outers: List[List[OSMNode]],
line: Union[OSMWay, OSMRelation],
inners: list[list[OSMNode]],
outers: list[list[OSMNode]],
) -> None:
"""
Way or relation construction.
@ -199,22 +191,22 @@ class Constructor:
"""
assert len(outers) >= 1
if not self.check_level(line.tags):
return
center_point, center_coordinates = line_center(outers[0], self.flinger)
if self.mode in ["user-coloring", "time"]:
if self.mode == "user-coloring":
if self.mode in [AUTHOR_MODE, TIME_MODE]:
color: Color
if self.mode == AUTHOR_MODE:
color = get_user_color(line.user, self.seed)
else: # self.mode == "time":
color = get_time_color(line.timestamp, self.map_.time)
else: # self.mode == TIME_MODE
color = get_time_color(line.timestamp, self.osm_data.time)
self.draw_special_mode(inners, line, outers, color)
return
if not line.tags:
return
scale: float = self.flinger.get_scale(center_coordinates)
line_styles: List[LineStyle] = self.scheme.get_style(line.tags, scale)
if "building:part" in line.tags or "building" in line.tags:
self.add_building(
Building(line.tags, inners, outers, self.flinger, self.scheme)
@ -225,6 +217,9 @@ class Constructor:
self.roads.append(Road(line.tags, inners, outers, road_matcher))
return
scale: float = self.flinger.get_scale(center_coordinates)
line_styles: list[LineStyle] = self.scheme.get_style(line.tags, scale)
for line_style in line_styles:
self.figures.append(
StyledFigure(line.tags, inners, outers, line_style)
@ -235,7 +230,7 @@ class Constructor:
and line.get_tag("area") != "no"
and self.scheme.is_area(line.tags)
):
processed: Set[str] = set()
processed: set[str] = set()
priority: int
icon_set: IconSet
@ -257,7 +252,7 @@ class Constructor:
if not line_styles:
if DEBUG:
style: Dict[str, Any] = {
style: dict[str, Any] = {
"fill": "none",
"stroke": Color("red").hex,
"stroke-width": 1,
@ -267,7 +262,7 @@ class Constructor:
)
self.figures.append(figure)
processed: Set[str] = set()
processed: set[str] = set()
priority: int
icon_set: IconSet
@ -285,7 +280,7 @@ class Constructor:
"""
Add figure for special mode: time or author.
"""
style: Dict[str, Any] = {
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
"stroke-width": 1,
@ -298,84 +293,94 @@ class Constructor:
"""
Construct Röntgen ways from OSM relations.
"""
for relation_id in self.map_.relations:
relation: OSMRelation = self.map_.relations[relation_id]
for relation_id in self.osm_data.relations:
relation: OSMRelation = self.osm_data.relations[relation_id]
tags = relation.tags
if not self.check_level(tags):
continue
if "type" not in tags or tags["type"] != "multipolygon":
continue
inner_ways: List[OSMWay] = []
outer_ways: List[OSMWay] = []
inner_ways: list[OSMWay] = []
outer_ways: list[OSMWay] = []
for member in relation.members:
if member.type_ == "way":
if member.role == "inner":
if member.ref in self.map_.ways:
inner_ways.append(self.map_.ways[member.ref])
if member.ref in self.osm_data.ways:
inner_ways.append(self.osm_data.ways[member.ref])
elif member.role == "outer":
if member.ref in self.map_.ways:
outer_ways.append(self.map_.ways[member.ref])
if member.ref in self.osm_data.ways:
outer_ways.append(self.osm_data.ways[member.ref])
else:
logging.warning(f'Unknown member role "{member.role}".')
if outer_ways:
inners_path: List[List[OSMNode]] = glue(inner_ways)
outers_path: List[List[OSMNode]] = glue(outer_ways)
inners_path: list[list[OSMNode]] = glue(inner_ways)
outers_path: list[list[OSMNode]] = glue(outer_ways)
self.construct_line(relation, inners_path, outers_path)
def construct_nodes(self) -> None:
"""
Draw nodes.
"""
node_number: int = 0
sorted_node_ids: Iterator[int] = sorted(
self.map_.nodes.keys(),
key=lambda x: -self.map_.nodes[x].coordinates[0],
self.osm_data.nodes.keys(),
key=lambda x: -self.osm_data.nodes[x].coordinates[0],
)
for node_id in sorted_node_ids:
processed: Set[str] = set()
node_number += 1
for index, node_id in enumerate(sorted_node_ids):
ui.progress_bar(
node_number, len(self.map_.nodes), text="Constructing nodes"
index, len(self.osm_data.nodes), text="Constructing nodes"
)
node: OSMNode = self.map_.nodes[node_id]
flung = self.flinger.fling(node.coordinates)
tags = node.tags
self.construct_node(self.osm_data.nodes[node_id])
ui.progress_bar(-1, len(self.osm_data.nodes), text="Constructing nodes")
if not self.check_level(tags):
continue
def construct_node(self, node: OSMNode) -> None:
tags = node.tags
if not self.check_level(tags):
return
priority: int
icon_set: IconSet
draw_outline: bool = True
processed: set[str] = set()
if self.mode in ["time", "user-coloring"]:
if not tags:
continue
color = DEFAULT_COLOR
if self.mode == "user-coloring":
color = get_user_color(node.user, self.seed)
if self.mode == "time":
color = get_time_color(node.timestamp, self.map_.time)
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
icon_set = IconSet(
Icon([ShapeSpecification(dot, color)]), [], set()
)
priority = 0
draw_outline = False
labels = []
else:
icon_set, priority = self.scheme.get_icon(
self.icon_extractor, tags, processed
)
labels = self.scheme.construct_text(tags, "all", processed)
self.scheme.process_ignored(tags, processed)
flung = self.flinger.fling(node.coordinates)
priority: int
icon_set: IconSet
draw_outline: bool = True
if self.mode in [TIME_MODE, AUTHOR_MODE]:
if not tags:
return
color: Color = DEFAULT_COLOR
if self.mode == AUTHOR_MODE:
color = get_user_color(node.user, self.seed)
if self.mode == TIME_MODE:
color = get_time_color(node.timestamp, self.osm_data.time)
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
icon_set = IconSet(
Icon([ShapeSpecification(dot, color)]), [], set()
)
point: Point = Point(
icon_set, labels, tags, processed, flung, node.coordinates,
priority=priority, draw_outline=draw_outline
icon_set, [], tags, processed, flung, node.coordinates,
priority=0, draw_outline=False
) # fmt: skip
self.points.append(point)
return
ui.progress_bar(-1, len(self.map_.nodes), text="Constructing nodes")
icon_set, priority = self.scheme.get_icon(
self.icon_extractor, tags, processed
)
labels = self.scheme.construct_text(tags, "all", processed)
self.scheme.process_ignored(tags, processed)
if node.get_tag("natural") == "tree" and (
"diameter_crown" in node.tags or "circumference" in node.tags
):
self.trees.append(Tree(tags, node.coordinates, flung))
return
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,
priority=priority, draw_outline=draw_outline
) # fmt: skip
self.points.append(point)

View file

@ -1,7 +1,7 @@
"""
Direction tag support.
"""
from typing import Iterator, List, Optional, Union
from typing import Iterator, Optional, Union
import numpy as np
from portolan import middle
@ -72,7 +72,7 @@ class Sector:
self.main_direction: Optional[np.array] = None
if "-" in text:
parts: List[str] = text.split("-")
parts: list[str] = text.split("-")
self.start = parse_vector(parts[0])
self.end = parse_vector(parts[1])
self.main_direction = (self.start + self.end) / 2
@ -90,7 +90,7 @@ class Sector:
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[List[SVGPath]]:
def draw(self, center: np.array, radius: float) -> Optional[list[SVGPath]]:
"""
Construct SVG path commands for arc element.
@ -139,7 +139,7 @@ class DirectionSet:
def __str__(self) -> str:
return ", ".join(map(str, self.sectors))
def draw(self, center: np.array, radius: float) -> Iterator[List[SVGPath]]:
def draw(self, center: np.array, radius: float) -> Iterator[list[SVGPath]]:
"""
Construct SVG "d" for arc elements.
@ -159,7 +159,7 @@ class DirectionSet:
:return: true if direction is right, false if direction is left, and
None otherwise.
"""
result: List[bool] = [x.is_right() for x in self.sectors]
result: list[bool] = [x.is_right() for x in self.sectors]
if result == [True] * len(result):
return True
if result == [False] * len(result):

View file

@ -9,10 +9,13 @@ import cairo
import numpy as np
import svgwrite
from colour import Color
from svgwrite.shapes import Rect
from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect
from svgwrite.text import Text
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
@dataclass
class Style:

View file

@ -4,7 +4,11 @@ Figures displayed on the map.
from typing import Any, Dict, List, Optional
import numpy as np
from colour import Color
from svgwrite import Drawing
from svgwrite.path import Path
from roentgen.direction import Sector, DirectionSet
from roentgen.flinger import Flinger
from roentgen.osm_reader import OSMNode, Tagged
from roentgen.road import Lane
@ -97,14 +101,85 @@ class Building(Figure):
if levels:
self.min_height = float(levels) * 2.5
height: Optional[str] = self.get_length("height")
height: Optional[float] = self.get_length("height")
if height:
self.height = height
height: Optional[str] = self.get_length("min_height")
height: Optional[float] = self.get_length("min_height")
if height:
self.min_height = height
def draw_shade(self, building_shade, 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))
commands: str = self.get_path(flinger, shift_1)
path = Path(
d=commands, fill="#000000", stroke="#000000", stroke_width=1
)
building_shade.add(path)
for nodes in self.inners + self.outers:
for i in range(len(nodes) - 1):
flung_1 = flinger.fling(nodes[i].coordinates)
flung_2 = flinger.fling(nodes[i + 1].coordinates)
command = (
"M",
np.add(flung_1, shift_1),
"L",
np.add(flung_2, shift_1),
np.add(flung_2, shift_2),
np.add(flung_1, shift_2),
"Z",
)
path = Path(
command, fill="#000000", stroke="#000000", stroke_width=1
)
building_shade.add(path)
def draw_walls(
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]
for segment in self.parts:
if height == 2:
fill = Color("#AAAAAA")
elif height == 4:
fill = Color("#C3C3C3")
else:
color_part: float = 0.8 + segment.angle * 0.2
fill = Color(rgb=(color_part, color_part, color_part))
command = (
"M",
segment.point_1 + shift_1,
"L",
segment.point_2 + shift_1,
segment.point_2 + shift_2,
segment.point_1 + shift_2,
segment.point_1 + shift_1,
"Z",
)
path = svg.path(
d=command,
fill=fill.hex,
stroke=fill.hex,
stroke_width=1,
stroke_linejoin="round",
)
svg.add(path)
def draw_roof(self, svg: Drawing, flinger: Flinger, scale: float):
"""Draw building roof."""
path: Path = Path(
d=self.get_path(flinger, np.array([0, -self.height * scale]))
)
path.update(self.line_style.style)
path.update({"stroke-linejoin": "round"})
svg.add(path)
class StyledFigure(Figure):
"""
@ -161,14 +236,110 @@ class Road(Figure):
pass
class Tree(Tagged):
"""
Tree on the map.
"""
def __init__(
self, tags: dict[str, str], coordinates: np.array, point: np.array
):
super().__init__(tags)
self.coordinates: np.array = coordinates
self.point: np.array = point
def draw(self, svg: Drawing, flinger: Flinger, scheme: Scheme):
"""Draw crown and trunk."""
scale: float = flinger.get_scale(self.coordinates)
radius: float
if "diameter_crown" in self.tags:
radius = float(self.tags["diameter_crown"]) / 2.0
else:
radius = 2.0
color: Color = scheme.get_color("evergreen_color")
svg.add(svg.circle(self.point, radius * scale, fill=color, opacity=0.3))
if "circumference" in self.tags:
radius: float = float(self.tags["circumference"]) / 2.0 / np.pi
svg.add(svg.circle(self.point, radius * scale, fill="#B89A74"))
class DirectionSector(Tagged):
"""
Sector that represents direction.
"""
def __init__(self, tags: dict[str, str], point):
super().__init__(tags)
self.point = point
def draw(self, svg: Drawing, scheme: Scheme):
"""Draw gradient sector."""
angle = None
is_revert_gradient: bool = False
if self.get_tag("man_made") == "surveillance":
direction = self.get_tag("camera:direction")
if "camera:angle" in self.tags:
angle = float(self.get_tag("camera:angle"))
if "angle" in self.tags:
angle = float(self.get_tag("angle"))
direction_radius: float = 25
direction_color: 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")
else:
direction = self.get_tag("direction")
direction_radius: float = 50
direction_color: Color = scheme.get_color("direction_view_color")
is_revert_gradient = True
if not direction:
return
point = (self.point.astype(int)).astype(float)
if angle:
paths = [Sector(direction, angle).draw(point, direction_radius)]
else:
paths = DirectionSet(direction).draw(point, direction_radius)
for path in paths:
radial_gradient = svg.radialGradient(
center=point,
r=direction_radius,
gradientUnits="userSpaceOnUse",
)
gradient = svg.defs.add(radial_gradient)
if is_revert_gradient:
(
gradient
.add_stop_color(0, direction_color.hex, opacity=0)
.add_stop_color(1, direction_color.hex, opacity=0.7)
) # fmt: skip
else:
(
gradient
.add_stop_color(0, direction_color.hex, opacity=0.4)
.add_stop_color(1, direction_color.hex, opacity=0)
) # fmt: skip
path_element: Path = svg.path(
d=["M", point] + path + ["L", point, "Z"],
fill=gradient.get_paint_server(),
)
svg.add(path_element)
class Segment:
"""
Line segment.
"""
def __init__(self, point_1: np.array, point_2: np.array):
self.point_1 = point_1
self.point_2 = point_2
self.point_1: np.array = point_1
self.point_2: np.array = point_2
difference: np.array = point_2 - point_1
vector: np.array = difference / np.linalg.norm(difference)
@ -215,9 +386,7 @@ def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
"""
Construct SVG path commands from nodes.
"""
"""Construct SVG path commands from nodes."""
path: str = ""
prev_node: Optional[OSMNode] = None
for node in nodes:

View file

@ -27,10 +27,13 @@ def pseudo_mercator(coordinates: np.array) -> np.array:
return np.array((coordinates[1], y))
def osm_zoom_level_to_pixels_per_meter(zoom_level: float):
def osm_zoom_level_to_pixels_per_meter(zoom_level: float) -> float:
"""
Convert OSM zoom level (see https://wiki.openstreetmap.org/wiki/Zoom_levels)
to pixels per meter on Equator.
Convert OSM zoom level to pixels per meter on Equator. See
https://wiki.openstreetmap.org/wiki/Zoom_levels
:param zoom_level: integer number usually not bigger than 20, but this
function allows any non-negative float value
"""
return 2 ** zoom_level / 156415

View file

@ -4,7 +4,7 @@ Icon grid drawing.
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Set
from typing import Optional
import numpy as np
from colour import Color
@ -24,7 +24,7 @@ class IconCollection:
Collection of icons.
"""
icons: List[Icon]
icons: list[Icon]
@classmethod
def from_scheme(
@ -45,7 +45,7 @@ class IconCollection:
:param add_unused: create icons from shapes that have no corresponding
tags
"""
icons: List[Icon] = []
icons: list[Icon] = []
def add() -> Icon:
"""
@ -80,7 +80,7 @@ class IconCollection:
continue
for icon_id in matcher.under_icon:
for icon_2_id in matcher.with_icon:
current_set: List[str] = (
current_set: list[str] = (
[icon_id] + [icon_2_id] + matcher.over_icon
)
add()
@ -99,7 +99,7 @@ class IconCollection:
):
add()
specified_ids: Set[str] = set()
specified_ids: set[str] = set()
for icon in icons:
specified_ids |= set(icon.get_shape_ids())
@ -199,17 +199,18 @@ def draw_icons() -> None:
Draw all possible icon shapes combinations as grid in one SVG file and as
individual SVG files.
"""
icons_by_id_path: Path = workspace.get_icons_by_id_path()
icons_by_name_path: Path = workspace.get_icons_by_name_path()
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
collection: IconCollection = IconCollection.from_scheme(scheme, extractor)
icon_grid_path: Path = workspace.get_icon_grid_path()
collection.draw_grid(icon_grid_path)
logging.info(f"Icon grid is written to {icon_grid_path}.")
icons_by_id_path: Path = workspace.get_icons_by_id_path()
icons_by_name_path: Path = workspace.get_icons_by_name_path()
collection.draw_icons(icons_by_id_path)
collection.draw_icons(icons_by_name_path, by_name=True)
logging.info(

View file

@ -6,7 +6,7 @@ import logging
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from typing import Any, Optional
from xml.dom.minidom import Document, Element, parse
import numpy as np
@ -44,13 +44,13 @@ class Shape:
id_: str # shape identifier
name: Optional[str] = None # icon description
is_right_directed: Optional[bool] = None
emojis: Set[str] = field(default_factory=set)
emojis: set[str] = field(default_factory=set)
is_part: bool = False
@classmethod
def from_structure(
cls,
structure: Dict[str, Any],
structure: dict[str, Any],
path: str,
offset: np.array,
id_: str,
@ -102,7 +102,7 @@ class Shape:
:param offset: additional offset
:param scale: scale resulting image
"""
transformations: List[str] = []
transformations: list[str] = []
shift: np.array = point + offset
transformations.append(f"translate({shift[0]},{shift[1]})")
@ -137,7 +137,7 @@ def verify_sketch_element(element, id_: str) -> bool:
if not element.getAttribute("style"):
return True
style: Dict = dict(
style: dict = dict(
[x.split(":") for x in element.getAttribute("style").split(";")]
)
if (
@ -177,8 +177,8 @@ class ShapeExtractor:
:param svg_file_name: input SVG file name with icons. File may contain
any other irrelevant graphics.
"""
self.shapes: Dict[str, Shape] = {}
self.configuration: Dict[str, Any] = json.load(
self.shapes: dict[str, Shape] = {}
self.configuration: dict[str, Any] = json.load(
configuration_file_name.open()
)
with svg_file_name.open() as input_file:
@ -233,7 +233,7 @@ class ShapeExtractor:
name = child_node.childNodes[0].nodeValue
break
configuration: Dict[str, Any] = (
configuration: dict[str, Any] = (
self.configuration[id_] if id_ in self.configuration else {}
)
self.shapes[id_] = Shape.from_structure(
@ -326,7 +326,7 @@ class ShapeSpecification:
self,
svg,
point: np.array,
tags: Dict[str, Any] = None,
tags: dict[str, Any] = None,
outline: bool = False,
) -> None:
"""
@ -351,7 +351,7 @@ class ShapeSpecification:
bright: bool = is_bright(self.color)
color: Color = Color("black") if bright else Color("white")
style: Dict[str, Any] = {
style: dict[str, Any] = {
"fill": color.hex,
"stroke": color.hex,
"stroke-width": 2.2,
@ -381,15 +381,15 @@ class Icon:
Icon that consists of (probably) multiple shapes.
"""
shape_specifications: List[ShapeSpecification]
shape_specifications: list[ShapeSpecification]
def get_shape_ids(self) -> List[str]:
def get_shape_ids(self) -> list[str]:
"""
Get all shape identifiers in the icon.
"""
return [x.shape.id_ for x in self.shape_specifications]
def get_names(self) -> List[str]:
def get_names(self) -> list[str]:
"""
Gat all shape names in the icon.
"""
@ -402,7 +402,7 @@ class Icon:
self,
svg: svgwrite.Drawing,
point: np.array,
tags: Dict[str, Any] = None,
tags: dict[str, Any] = None,
outline: bool = False,
) -> None:
"""
@ -469,7 +469,7 @@ class Icon:
shape_specification.color = color
def add_specifications(
self, specifications: List[ShapeSpecification]
self, specifications: list[ShapeSpecification]
) -> None:
"""
Add shape specifications to the icon.
@ -494,8 +494,8 @@ class IconSet:
"""
main_icon: Icon
extra_icons: List[Icon]
extra_icons: list[Icon]
# Tag keys that were processed to create icon set (other tag keys should be
# displayed by text or ignored)
processed: Set[str]
processed: set[str]

View file

@ -3,7 +3,7 @@ MapCSS scheme creation.
"""
import logging
from pathlib import Path
from typing import Dict, List, Optional, TextIO
from typing import Optional, TextIO
from colour import Color
@ -83,8 +83,8 @@ class MapCSSWriter:
self.add_icons_for_lifecycle: bool = add_icons_for_lifecycle
self.icon_directory_name: str = icon_directory_name
self.point_matchers: List[Matcher] = scheme.node_matchers
self.line_matchers: List[Matcher] = scheme.way_matchers
self.point_matchers: list[Matcher] = scheme.node_matchers
self.line_matchers: list[Matcher] = scheme.way_matchers
def add_selector(
self,
@ -102,7 +102,7 @@ class MapCSSWriter:
:param opacity: icon opacity
:return:
"""
elements: Dict[str, str] = {}
elements: dict[str, str] = {}
clean_shapes = matcher.get_clean_shapes()
if clean_shapes:

View file

@ -1,7 +1,7 @@
"""
Simple OpenStreetMap renderer.
"""
from typing import Any, Dict, Iterator, Set
from typing import Any, Iterator
import numpy as np
import svgwrite
@ -12,33 +12,27 @@ from svgwrite.shapes import Rect
from roentgen import ui
from roentgen.constructor import Constructor
from roentgen.direction import DirectionSet, Sector
from roentgen.figure import Road
from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor
from roentgen.osm_reader import Map, OSMNode
from roentgen.osm_reader import OSMNode
from roentgen.point import Occupied
from roentgen.road import Intersection, RoadPart
from roentgen.scheme import Scheme
from roentgen.ui import AUTHOR_MODE, TIME_MODE
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
AUTHOR_MODE = "user-coloring"
CREATION_TIME_MODE = "time"
class Painter:
class Map:
"""
Map drawing.
"""
def __init__(
self,
map_: Map,
flinger: Flinger,
svg: svgwrite.Drawing,
icon_extractor: ShapeExtractor,
scheme: Scheme,
overlap: int = 12,
mode: str = "normal",
@ -48,20 +42,16 @@ class Painter:
self.mode: str = mode
self.label_mode: str = label_mode
self.map_: Map = map_
self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg
self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme
self.background_color: Color = self.scheme.get_color("background_color")
if self.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
if self.mode in [AUTHOR_MODE, TIME_MODE]:
self.background_color: Color = Color("#111111")
def draw(self, constructor: Constructor) -> None:
"""
Draw map.
"""
"""Draw map."""
self.svg.add(
Rect((0, 0), self.flinger.size, fill=self.background_color)
)
@ -84,9 +74,13 @@ class Painter:
for road in roads:
self.draw_road(road, road.matcher.color)
self.draw_trees(constructor)
for tree in constructor.trees:
tree.draw(self.svg, self.flinger, self.scheme)
for direction_sector in constructor.direction_sectors:
direction_sector.draw(self.svg, self.scheme)
self.draw_buildings(constructor)
self.draw_direction(constructor)
# All other points
@ -101,10 +95,6 @@ class Painter:
steps: int = len(nodes)
for index, node in enumerate(nodes):
if node.get_tag("natural") == "tree" and (
"diameter_crown" in node.tags or "circumference" in node.tags
):
continue
ui.progress_bar(
index, steps * 3, step=10, text="Drawing main icons"
)
@ -121,207 +111,39 @@ class Painter:
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
)
if (
self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]
self.mode not in [TIME_MODE, AUTHOR_MODE]
and self.label_mode != "no"
):
point.draw_texts(self.svg, occupied, self.label_mode)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
def draw_trees(self, constructor) -> None:
"""
Draw trunk and circumference.
"""
for node in constructor.points:
if not (
node.get_tag("natural") == "tree"
and (
"diameter_crown" in node.tags
or "circumference" in node.tags
)
):
continue
scale: float = self.flinger.get_scale(node.coordinates)
if "circumference" in node.tags:
if "diameter_crown" in node.tags:
opacity = 0.7
radius = float(node.tags["diameter_crown"]) / 2
else:
opacity = 0.3
radius = 2
self.svg.add(
self.svg.circle(
node.point,
radius * scale,
fill=self.scheme.get_color("evergreen_color"),
opacity=opacity,
)
)
radius = float(node.tags["circumference"]) / 2 / np.pi
self.svg.add(
self.svg.circle(node.point, radius * scale, fill="#B89A74")
)
def draw_buildings(self, constructor: Constructor) -> None:
"""
Draw buildings: shade, walls, and roof.
"""
# Draw shade.
"""Draw buildings: shade, walls, and roof."""
building_shade: Group = Group(opacity=0.1)
scale: float = self.flinger.get_scale() / 3.0
for building in constructor.buildings:
shift_1 = np.array((scale * building.min_height, 0))
shift_2 = np.array((scale * building.height, 0))
commands: str = building.get_path(self.flinger, shift_1)
path = Path(
d=commands, fill="#000000", stroke="#000000", stroke_width=1
)
building_shade.add(path)
for nodes in building.inners + building.outers:
for i in range(len(nodes) - 1):
flung_1 = self.flinger.fling(nodes[i].coordinates)
flung_2 = self.flinger.fling(nodes[i + 1].coordinates)
command = (
"M",
np.add(flung_1, shift_1),
"L",
np.add(flung_2, shift_1),
np.add(flung_2, shift_2),
np.add(flung_1, shift_2),
"Z",
)
path = Path(
command,
fill="#000000",
stroke="#000000",
stroke_width=1,
)
building_shade.add(path)
building.draw_shade(building_shade, self.flinger)
self.svg.add(building_shade)
# Draw buildings.
previous_height: float = 0
count: int = len(constructor.heights)
for index, height in enumerate(sorted(constructor.heights)):
ui.progress_bar(index, count, step=1, text="Drawing buildings")
fill: Color()
for way in constructor.buildings:
if way.height < height or way.min_height > height:
for building in constructor.buildings:
if building.height < height or building.min_height > height:
continue
shift_1 = [0, -previous_height * scale]
shift_2 = [0, -height * scale]
for segment in way.parts:
if height == 2:
fill = Color("#AAAAAA")
elif height == 4:
fill = Color("#C3C3C3")
else:
color_part: float = 0.8 + segment.angle * 0.2
fill = Color(rgb=(color_part, color_part, color_part))
building.draw_walls(self.svg, height, previous_height, scale)
command = (
"M",
segment.point_1 + shift_1,
"L",
segment.point_2 + shift_1,
segment.point_2 + shift_2,
segment.point_1 + shift_2,
segment.point_1 + shift_1,
"Z",
)
path = self.svg.path(
d=command,
fill=fill.hex,
stroke=fill.hex,
stroke_width=1,
stroke_linejoin="round",
)
self.svg.add(path)
# Draw building roofs.
for way in constructor.buildings:
if way.height == height:
shift = np.array([0, -way.height * scale])
path_commands: str = way.get_path(self.flinger, shift)
path = Path(d=path_commands, opacity=1)
path.update(way.line_style.style)
path.update({"stroke-linejoin": "round"})
self.svg.add(path)
for building in constructor.buildings:
if building.height == height:
building.draw_roof(self.svg, self.flinger, scale)
previous_height = height
ui.progress_bar(-1, count, step=1, text="Drawing buildings")
def draw_direction(self, constructor) -> None:
"""
Draw gradient sectors for directions.
"""
for node in constructor.points:
angle = None
is_revert_gradient: bool = False
if node.get_tag("man_made") == "surveillance":
direction = node.get_tag("camera:direction")
if "camera:angle" in node.tags:
angle = float(node.get_tag("camera:angle"))
if "angle" in node.tags:
angle = float(node.get_tag("angle"))
direction_radius: float = 25
direction_color: Color = self.scheme.get_color(
"direction_camera_color"
)
elif node.get_tag("traffic_sign") == "stop":
direction = node.get_tag("direction")
direction_radius: float = 25
direction_color: Color = Color("red")
else:
direction = node.get_tag("direction")
direction_radius: float = 50
direction_color: Color = self.scheme.get_color(
"direction_view_color"
)
is_revert_gradient = True
if not direction:
continue
point = (node.point.astype(int)).astype(float)
if angle:
paths = [Sector(direction, angle).draw(point, direction_radius)]
else:
paths = DirectionSet(direction).draw(point, direction_radius)
for path in paths:
radial_gradient = self.svg.radialGradient(
center=point,
r=direction_radius,
gradientUnits="userSpaceOnUse",
)
gradient = self.svg.defs.add(radial_gradient)
if is_revert_gradient:
(
gradient
.add_stop_color(0, direction_color.hex, opacity=0)
.add_stop_color(1, direction_color.hex, opacity=0.7)
) # fmt: skip
else:
(
gradient
.add_stop_color(0, direction_color.hex, opacity=0.4)
.add_stop_color(1, direction_color.hex, opacity=0)
) # fmt: skip
path = self.svg.path(
d=["M", point] + path + ["L", point, "Z"],
fill=gradient.get_paint_server(),
)
self.svg.add(path)
def draw_road(
self, road: Road, color: Color, extra_width: float = 0
) -> None:
@ -336,7 +158,7 @@ class Painter:
scale = self.flinger.get_scale(road.outers[0][0].coordinates)
path_commands: str = road.get_path(self.flinger)
path = Path(d=path_commands)
style: Dict[str, Any] = {
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
"stroke-linecap": "round",
@ -350,7 +172,7 @@ class Painter:
"""
Draw road as simple SVG path.
"""
nodes: Dict[OSMNode, Set[RoadPart]] = {}
nodes: dict[OSMNode, set[RoadPart]] = {}
for road in roads:
for index in range(len(road.outers[0]) - 1):
@ -379,7 +201,7 @@ class Painter:
intersection.draw(self.svg, scale, True)
def check_level_number(tags: Dict[str, Any], level: float):
def check_level_number(tags: dict[str, Any], level: float):
"""
Check if element described by tags is no the specified level.
"""
@ -392,7 +214,7 @@ def check_level_number(tags: Dict[str, Any], level: float):
return True
def check_level_overground(tags: Dict[str, Any]) -> bool:
def check_level_overground(tags: dict[str, Any]) -> bool:
"""
Check if element described by tags is overground.
"""

View file

@ -4,7 +4,7 @@ Moire markup extension for Röntgen.
import argparse
from abc import ABC
from pathlib import Path
from typing import Any, Dict, List, Union
from typing import Any, Union
import yaml
from moire.default import Default, DefaultHTML, DefaultMarkdown, DefaultWiki
@ -17,8 +17,8 @@ from roentgen.workspace import workspace
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
Arguments = List[Any]
Code = Union[str, Tag, List]
Arguments = list[Any]
Code = Union[str, Tag, list]
PREFIX: str = "https://wiki.openstreetmap.org/wiki/"
@ -29,13 +29,13 @@ class ArgumentParser(argparse.ArgumentParser):
"""
def __init__(self, *args, **kwargs):
self.arguments: List[Dict[str, Any]] = []
self.arguments: list[dict[str, Any]] = []
super(ArgumentParser, self).__init__(*args, **kwargs)
def add_argument(self, *args, **kwargs) -> None:
"""Just store argument with options."""
super(ArgumentParser, self).add_argument(*args, **kwargs)
argument: Dict[str, Any] = {"arguments": [x for x in args]}
argument: dict[str, Any] = {"arguments": [x for x in args]}
for key in kwargs:
argument[key] = kwargs[key]
@ -57,7 +57,7 @@ class ArgumentParser(argparse.ArgumentParser):
row: Code = [[x for y in array for x in y][:-1]]
if "help" in option:
help_value: List = [option["help"]]
help_value: list = [option["help"]]
if (
"default" in option
and option["default"]
@ -91,13 +91,13 @@ class TestConfiguration:
"""
def __init__(self, test_config: Path):
self.steps: Dict[str, Any] = {}
self.steps: dict[str, Any] = {}
with test_config.open() as input_file:
content: Dict[str, Any] = yaml.load(
content: dict[str, Any] = yaml.load(
input_file, Loader=yaml.FullLoader
)
steps: List[Dict[str, Any]] = content["jobs"]["build"]["steps"]
steps: list[dict[str, Any]] = content["jobs"]["build"]["steps"]
for step in steps:
if "name" not in step:
continue
@ -177,7 +177,8 @@ class RoentgenHTML(RoentgenMoire, DefaultHTML):
Simple HTML.
"""
images = {}
def __init__(self):
self.images: dict = {}
def color(self, args: Arguments) -> str:
"""Simple color sample."""
@ -202,10 +203,11 @@ class RoentgenOSMWiki(RoentgenMoire, DefaultWiki):
See https://wiki.openstreetmap.org/wiki/Main_Page
"""
images = {}
extractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
def __init__(self):
self.images: dict = {}
self.extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
def osm(self, args: Arguments) -> str:
"""OSM tag key or keyvalue pair of tag."""

View file

@ -4,8 +4,8 @@ Getting OpenStreetMap data from the web.
import logging
import time
import urllib
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional
import urllib3
@ -14,44 +14,44 @@ from roentgen.ui import BoundaryBox
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
SLEEP_TIME_BETWEEN_REQUESTS: float = 2.0
@dataclass
class NetworkError(Exception):
"""Failed network request."""
def __init__(self, message: str):
super().__init__()
self.message: str = message
message: str
def get_osm(
boundary_box: BoundaryBox, cache_path: Path, to_update: bool = False
boundary_box: BoundaryBox, cache_file_path: Path, to_update: bool = False
) -> str:
"""
Download OSM data from the web or get if from the cache.
:param boundary_box: borders of the map part to download
:param cache_path: cache directory to store downloaded OSM files
:param cache_file_path: cache file to store downloaded OSM data
:param to_update: update cache files
"""
result_file_name: Path = cache_path / f"{boundary_box.get_format()}.osm"
if not to_update and cache_file_path.is_file():
with cache_file_path.open() as output_file:
return output_file.read()
if not to_update and result_file_name.is_file():
return result_file_name.open().read()
content: Optional[str] = get_data(
content: str = get_data(
"api.openstreetmap.org/api/0.6/map",
{"bbox": boundary_box.get_format()},
is_secure=True,
).decode("utf-8")
with result_file_name.open("w+") as output_file:
with cache_file_path.open("w+") as output_file:
output_file.write(content)
return content
def get_data(
address: str, parameters: Dict[str, str], is_secure: bool = False
address: str, parameters: dict[str, str], is_secure: bool = False
) -> bytes:
"""
Construct Internet page URL and get its descriptor.
@ -61,7 +61,7 @@ def get_data(
:param is_secure: https or http
:return: connection descriptor
"""
url: str = "http" + ("s" if is_secure else "") + "://" + address
url: str = f"http{('s' if is_secure else '')}://{address}"
if len(parameters) > 0:
url += f"?{urllib.parse.urlencode(parameters)}"
logging.info(f"Getting {url}...")
@ -74,5 +74,5 @@ def get_data(
raise NetworkError("Cannot download data: too many attempts.")
pool_manager.clear()
time.sleep(2)
time.sleep(SLEEP_TIME_BETWEEN_REQUESTS)
return result.data

View file

@ -6,7 +6,7 @@ import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from typing import Any, Optional
from xml.etree import ElementTree
import numpy as np
@ -23,7 +23,8 @@ KILOMETERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$")
MILES_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$")
STAGES_OF_DECAY: List[str] = [
# See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay
STAGES_OF_DECAY: list[str] = [
"disused",
"abandoned",
"ruins",
@ -50,8 +51,9 @@ class Tagged:
OpenStreetMap element (node, way or relation) with tags.
"""
def __init__(self):
self.tags: Dict[str, str] = {}
def __init__(self, tags: dict[str, str] = None):
self.tags: dict[str, str]
self.tags = {} if tags is None else tags
def get_tag(self, key: str) -> Optional[str]:
"""
@ -139,7 +141,7 @@ class OSMNode(Tagged):
node.tags[subattributes["k"]] = subattributes["v"]
return node
def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMNode":
def parse_from_structure(self, structure: dict[str, Any]) -> "OSMNode":
"""
Parse node from Overpass-like structure.
@ -160,11 +162,11 @@ class OSMWay(Tagged):
See https://wiki.openstreetmap.org/wiki/Way
"""
def __init__(self, id_: int = 0, nodes: Optional[List[OSMNode]] = None):
def __init__(self, id_: int = 0, nodes: Optional[list[OSMNode]] = None):
super().__init__()
self.id_: int = id_
self.nodes: List[OSMNode] = [] if nodes is None else nodes
self.nodes: list[OSMNode] = [] if nodes is None else nodes
self.visible: Optional[str] = None
self.changeset: Optional[str] = None
@ -194,7 +196,7 @@ class OSMWay(Tagged):
return way
def parse_from_structure(
self, structure: Dict[str, Any], nodes
self, structure: dict[str, Any], nodes
) -> "OSMWay":
"""
Parse way from Overpass-like structure.
@ -245,7 +247,7 @@ class OSMRelation(Tagged):
super().__init__()
self.id_: int = id_
self.members: List["OSMMember"] = []
self.members: list["OSMMember"] = []
self.user: Optional[str] = None
self.timestamp: Optional[datetime] = None
@ -275,7 +277,7 @@ class OSMRelation(Tagged):
relation.tags[subelement.attrib["k"]] = subelement.attrib["v"]
return relation
def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMRelation":
def parse_from_structure(self, structure: dict[str, Any]) -> "OSMRelation":
"""
Parse relation from Overpass-like structure.
@ -305,19 +307,19 @@ class OSMMember:
role: str = ""
class Map:
class OSMData:
"""
The whole OpenStreetMap information about nodes, ways, and relations.
"""
def __init__(self):
self.nodes: Dict[int, OSMNode] = {}
self.ways: Dict[int, OSMWay] = {}
self.relations: Dict[int, OSMRelation] = {}
self.nodes: dict[int, OSMNode] = {}
self.ways: dict[int, OSMWay] = {}
self.relations: dict[int, OSMRelation] = {}
self.authors: Set[str] = set()
self.authors: set[str] = set()
self.time: MinMax = MinMax()
self.boundary_box: List[MinMax] = [MinMax(), MinMax()]
self.boundary_box: list[MinMax] = [MinMax(), MinMax()]
self.view_box = None
def add_node(self, node: OSMNode) -> None:
@ -355,9 +357,9 @@ class OverpassReader:
"""
def __init__(self):
self.map_ = Map()
self.osm_data = OSMData()
def parse_json_file(self, file_name: Path) -> Map:
def parse_json_file(self, file_name: Path) -> OSMData:
"""
Parse JSON structure from the file and construct map.
"""
@ -371,18 +373,18 @@ class OverpassReader:
if element["type"] == "node":
node = OSMNode().parse_from_structure(element)
node_map[node.id_] = node
self.map_.add_node(node)
self.osm_data.add_node(node)
for element in structure["elements"]:
if element["type"] == "way":
way = OSMWay().parse_from_structure(element, node_map)
way_map[way.id_] = way
self.map_.add_way(way)
self.osm_data.add_way(way)
for element in structure["elements"]:
if element["type"] == "relation":
relation = OSMRelation().parse_from_structure(element)
self.map_.add_relation(relation)
self.osm_data.add_relation(relation)
return self.map_
return self.osm_data
class OSMReader:
@ -406,13 +408,13 @@ class OSMReader:
:param is_full: whether metadata should be parsed: tags `visible`,
`changeset`, `timestamp`, `user`, `uid`
"""
self.map_ = Map()
self.osm_data = OSMData()
self.parse_nodes: bool = parse_nodes
self.parse_ways: bool = parse_ways
self.parse_relations: bool = parse_relations
self.is_full: bool = is_full
def parse_osm_file(self, file_name: Path) -> Map:
def parse_osm_file(self, file_name: Path) -> OSMData:
"""
Parse OSM XML file.
@ -421,7 +423,7 @@ class OSMReader:
"""
return self.parse_osm(ElementTree.parse(file_name).getroot())
def parse_osm_text(self, text: str) -> Map:
def parse_osm_text(self, text: str) -> OSMData:
"""
Parse OSM XML data from text representation.
@ -430,7 +432,7 @@ class OSMReader:
"""
return self.parse_osm(ElementTree.fromstring(text))
def parse_osm(self, root) -> Map:
def parse_osm(self, root) -> OSMData:
"""
Parse OSM XML data.
@ -442,25 +444,25 @@ class OSMReader:
self.parse_bounds(element)
if element.tag == "node" and self.parse_nodes:
node = OSMNode.from_xml_structure(element, self.is_full)
self.map_.add_node(node)
self.osm_data.add_node(node)
if element.tag == "way" and self.parse_ways:
self.map_.add_way(
self.osm_data.add_way(
OSMWay.from_xml_structure(
element, self.map_.nodes, self.is_full
element, self.osm_data.nodes, self.is_full
)
)
if element.tag == "relation" and self.parse_relations:
self.map_.add_relation(
self.osm_data.add_relation(
OSMRelation.from_xml_structure(element, self.is_full)
)
return self.map_
return self.osm_data
def parse_bounds(self, element) -> None:
"""
Parse view box from XML element.
"""
attributes = element.attrib
self.map_.view_box = MinMax(
self.osm_data.view_box = MinMax(
np.array(
(float(attributes["minlat"]), float(attributes["minlon"]))
),

View file

@ -1,7 +1,7 @@
"""
Point: node representation on the map.
"""
from typing import Dict, List, Optional, Set
from typing import Optional
import numpy as np
import svgwrite
@ -56,9 +56,9 @@ class Point(Tagged):
def __init__(
self,
icon_set: IconSet,
labels: List[Label],
tags: Dict[str, str],
processed: Set[str],
labels: list[Label],
tags: dict[str, str],
processed: set[str],
point: np.array,
coordinates: np.array,
priority: float = 0,
@ -70,9 +70,9 @@ class Point(Tagged):
assert point is not None
self.icon_set: IconSet = icon_set
self.labels: List[Label] = labels
self.tags: Dict[str, str] = tags
self.processed: Set[str] = processed
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.priority: float = priority
@ -139,7 +139,7 @@ class Point(Tagged):
icon: Icon,
position,
occupied,
tags: Optional[Dict[str, str]] = None,
tags: Optional[dict[str, str]] = None,
) -> bool:
"""
Draw one combined icon and its outline.
@ -176,7 +176,7 @@ class Point(Tagged):
"""
Draw all labels.
"""
labels: List[Label]
labels: list[Label]
if label_mode == "main":
labels = self.labels[:1]

View file

@ -5,7 +5,6 @@ import logging
import os
import subprocess
from pathlib import Path
from typing import List
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
@ -25,7 +24,7 @@ def rasterize(from_: Path, to_: Path, area: str = "", dpi: float = 90) -> None:
f"Inkscape and set the variable to be able to rasterize SVG files."
)
commands: List[str] = [os.environ[INKSCAPE_BIN]]
commands: list[str] = [os.environ[INKSCAPE_BIN]]
commands += ["--export-png", to_.absolute()]
commands += ["--export-dpi", str(dpi)]
if area:

View file

@ -2,7 +2,7 @@
WIP: road shape drawing.
"""
from dataclasses import dataclass
from typing import List, Optional
from typing import Optional
import numpy as np
import svgwrite
@ -48,7 +48,7 @@ class RoadPart:
self,
point_1: np.array,
point_2: np.array,
lanes: List[Lane],
lanes: list[Lane],
scale: False,
):
"""
@ -58,7 +58,7 @@ class RoadPart:
"""
self.point_1: np.array = point_1
self.point_2: np.array = point_2
self.lanes: List[Lane] = lanes
self.lanes: list[Lane] = lanes
if lanes:
self.width = sum(map(lambda x: x.get_width(scale), lanes))
else:
@ -297,8 +297,8 @@ class Intersection:
points of the road parts should be the same.
"""
def __init__(self, parts: List[RoadPart]):
self.parts: List[RoadPart] = sorted(parts, key=lambda x: x.get_angle())
def __init__(self, parts: list[RoadPart]):
self.parts: list[RoadPart] = sorted(parts, key=lambda x: x.get_angle())
for index in range(len(self.parts)):
next_index: int = 0 if index == len(self.parts) - 1 else index + 1

View file

@ -4,7 +4,7 @@ Röntgen drawing scheme.
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from typing import Any, Optional, Tuple, Union
import yaml
from colour import Color
@ -30,7 +30,7 @@ class LineStyle:
SVG line style and its priority.
"""
style: Dict[str, Union[int, float, str]]
style: dict[str, Union[int, float, str]]
priority: float = 0.0
@ -48,7 +48,7 @@ class MatchingType(Enum):
def is_matched_tag(
matcher_tag_key: str,
matcher_tag_value: Union[str, list],
tags: Dict[str, str],
tags: dict[str, str],
) -> MatchingType:
"""
Check whether element tags contradict tag matcher.
@ -89,10 +89,10 @@ class Matcher:
Tag matching.
"""
def __init__(self, structure: Dict[str, Any]):
self.tags: Dict[str, str] = structure["tags"]
def __init__(self, structure: dict[str, Any]):
self.tags: dict[str, str] = structure["tags"]
self.exception: Dict[str, str] = {}
self.exception: dict[str, str] = {}
if "exception" in structure:
self.exception = structure["exception"]
@ -100,11 +100,11 @@ class Matcher:
if "replace_shapes" in structure:
self.replace_shapes = structure["replace_shapes"]
self.location_restrictions: Dict[str, str] = {}
self.location_restrictions: dict[str, str] = {}
if "location_restrictions" in structure:
self.location_restrictions = structure["location_restrictions"]
def is_matched(self, tags: Dict[str, str]) -> bool:
def is_matched(self, tags: dict[str, str]) -> bool:
"""
Check whether element tags matches tag matcher.
@ -157,7 +157,7 @@ class NodeMatcher(Matcher):
Tag specification matcher.
"""
def __init__(self, structure: Dict[str, Any]):
def __init__(self, structure: dict[str, Any]):
# Dictionary with tag keys and values, value lists, or "*"
super().__init__(structure)
@ -200,11 +200,11 @@ class WayMatcher(Matcher):
Special tag matcher for ways.
"""
def __init__(self, structure: Dict[str, Any], scheme: "Scheme"):
def __init__(self, structure: dict[str, Any], scheme: "Scheme"):
super().__init__(structure)
self.style: Dict[str, Any] = {"fill": "none"}
self.style: dict[str, Any] = {"fill": "none"}
if "style" in structure:
style: Dict[str, Any] = structure["style"]
style: dict[str, Any] = structure["style"]
for key in style:
if str(style[key]).endswith("_color"):
self.style[key] = scheme.get_color(style[key]).hex.upper()
@ -223,7 +223,7 @@ class RoadMatcher(Matcher):
Special tag matcher for highways.
"""
def __init__(self, structure: Dict[str, Any], scheme: "Scheme"):
def __init__(self, structure: dict[str, Any], scheme: "Scheme"):
super().__init__(structure)
self.border_color: Color = Color(
scheme.get_color(structure["border_color"])
@ -250,33 +250,33 @@ class Scheme:
specification
"""
with file_name.open() as input_file:
content: Dict[str, Any] = yaml.load(
content: dict[str, Any] = yaml.load(
input_file.read(), Loader=yaml.FullLoader
)
self.node_matchers: List[NodeMatcher] = []
self.node_matchers: list[NodeMatcher] = []
for group in content["node_icons"]:
for element in group["tags"]:
self.node_matchers.append(NodeMatcher(element))
self.colors: Dict[str, str] = content["colors"]
self.material_colors: Dict[str, str] = content["material_colors"]
self.colors: dict[str, str] = content["colors"]
self.material_colors: dict[str, str] = content["material_colors"]
self.way_matchers: List[WayMatcher] = [
self.way_matchers: list[WayMatcher] = [
WayMatcher(x, self) for x in content["ways"]
]
self.road_matchers: List[RoadMatcher] = [
self.road_matchers: list[RoadMatcher] = [
RoadMatcher(x, self) for x in content["roads"]
]
self.area_matchers: List[Matcher] = [
self.area_matchers: list[Matcher] = [
Matcher(x) for x in content["area_tags"]
]
self.tags_to_write: List[str] = content["tags_to_write"]
self.prefix_to_write: List[str] = content["prefix_to_write"]
self.tags_to_skip: List[str] = content["tags_to_skip"]
self.prefix_to_skip: List[str] = content["prefix_to_skip"]
self.tags_to_write: list[str] = content["tags_to_write"]
self.prefix_to_write: list[str] = content["prefix_to_write"]
self.tags_to_skip: list[str] = content["tags_to_skip"]
self.prefix_to_skip: list[str] = content["prefix_to_skip"]
# Storage for created icon sets.
self.cache: Dict[str, Tuple[IconSet, int]] = {}
self.cache: dict[str, Tuple[IconSet, int]] = {}
def get_color(self, color: str) -> Color:
"""
@ -327,8 +327,8 @@ class Scheme:
def get_icon(
self,
extractor: ShapeExtractor,
tags: Dict[str, Any],
processed: Set[str],
tags: dict[str, Any],
processed: set[str],
for_: str = "node",
) -> Tuple[IconSet, int]:
"""
@ -347,7 +347,7 @@ class Scheme:
return self.cache[tags_hash]
main_icon: Optional[Icon] = None
extra_icons: List[Icon] = []
extra_icons: list[Icon] = []
priority: int = 0
index: int = 0
@ -358,7 +358,7 @@ class Scheme:
matched: bool = matcher.is_matched(tags)
if not matched:
continue
matcher_tags: Set[str] = set(matcher.tags.keys())
matcher_tags: set[str] = set(matcher.tags.keys())
priority = len(self.node_matchers) - index
if not matcher.draw:
processed |= matcher_tags
@ -437,7 +437,7 @@ class Scheme:
return returned, priority
def get_style(self, tags: Dict[str, Any], scale):
def get_style(self, tags: dict[str, Any], scale):
"""
Get line style based on tags and scale.
"""
@ -451,7 +451,7 @@ class Scheme:
return line_styles
def get_road(self, tags: Dict[str, Any]) -> Optional[RoadMatcher]:
def get_road(self, tags: dict[str, Any]) -> Optional[RoadMatcher]:
for matcher in self.road_matchers:
if not matcher.is_matched(tags):
continue
@ -459,12 +459,12 @@ class Scheme:
return None
def construct_text(
self, tags: Dict[str, str], draw_captions: str, processed: Set[str]
) -> List[Label]:
self, tags: dict[str, str], draw_captions: str, processed: set[str]
) -> list[Label]:
"""
Construct labels for not processed tags.
"""
texts: List[Label] = []
texts: list[Label] = []
name = None
alt_name = None
@ -490,7 +490,7 @@ class Scheme:
alt_name = ""
alt_name += "ex " + tags["old_name"]
address: List[str] = get_address(tags, draw_captions, processed)
address: list[str] = get_address(tags, draw_captions, processed)
if name:
texts.append(Label(name, Color("black")))
@ -535,7 +535,7 @@ class Scheme:
texts.append(Label(tags[tag]))
return texts
def is_area(self, tags: Dict[str, str]) -> bool:
def is_area(self, tags: dict[str, str]) -> bool:
"""
Check whether way described by tags is area.
"""
@ -545,7 +545,7 @@ class Scheme:
return False
def process_ignored(
self, tags: Dict[str, str], processed: Set[str]
self, tags: dict[str, str], processed: set[str]
) -> None:
"""
Mark all ignored tag as processed.
@ -553,6 +553,4 @@ class Scheme:
:param tags: input tag dictionary
:param processed: processed set
"""
for tag in tags:
if self.is_no_drawable(tag):
processed.add(tag)
[processed.add(tag) for tag in tags if self.is_no_drawable(tag)]

View file

@ -12,26 +12,23 @@ __email__ = "me@enzet.ru"
class Handler(BaseHTTPRequestHandler):
update_cache: bool = False
"""
HTTP request handler that process sloppy map tile requests.
"""
def __init__(self, request, client_address, server):
super().__init__(request, client_address, server)
self.cache: Path = Path("cache")
self.update_cache: bool = False
def write(self, message):
if isinstance(message, bytes):
self.wfile.write(message)
else:
self.wfile.write(message.encode("utf-8"))
def do_GET(self):
parts = self.path.split("/")
def do_GET(self) -> None:
"""Serve a GET request."""
parts: list[str] = self.path.split("/")
if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"):
return
zoom = int(parts[2])
x = int(parts[3])
y = int(parts[4])
zoom: int = int(parts[2])
x: int = int(parts[3])
y: int = int(parts[4])
tile_path: Path = workspace.get_tile_path()
png_path = tile_path / f"tile_{zoom}_{x}_{y}.png"
if self.update_cache:
@ -48,7 +45,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_response(200)
self.send_header("Content-type", "image/png")
self.end_headers()
self.write(input_file.read())
self.wfile.write(input_file.read())
return
@ -56,7 +53,7 @@ def ui(options):
server: Optional[HTTPServer] = None
try:
port: int = 8080
server = HTTPServer(("", port), Handler)
server: HTTPServer = HTTPServer(("", port), Handler)
server.cache_path = Path(options.cache)
server.serve_forever()
logging.info(f"Server started on port {port}.")

View file

@ -7,7 +7,6 @@ import json
import logging
from datetime import datetime
from pathlib import Path
from typing import List
from roentgen import (
__author__,
@ -55,7 +54,7 @@ class TaginfoProjectFile:
):
key: str = list(matcher.tags.keys())[0]
value: str = matcher.tags[key]
ids: List[str] = [
ids: list[str] = [
(x if isinstance(x, str) else x["shape"])
for x in matcher.shapes
]

View file

@ -2,7 +2,7 @@
OSM address tag processing.
"""
from dataclasses import dataclass
from typing import Any, Dict, List, Set
from typing import Any
from colour import Color
@ -24,15 +24,15 @@ class Label:
def get_address(
tags: Dict[str, Any], draw_captions_mode: str, processed: Set[str]
) -> List[str]:
tags: dict[str, Any], draw_captions_mode: str, processed: set[str]
) -> list[str]:
"""
Construct address text list from the tags.
:param tags: OSM node, way or relation tags
:param draw_captions_mode: captions mode ("all", "main", or "no")
"""
address: List[str] = []
address: list[str] = []
if draw_captions_mode == "address":
if "addr:postcode" in tags:
@ -80,12 +80,12 @@ def format_frequency(value: str) -> str:
return f"{value} "
def get_text(tags: Dict[str, Any], processed: Set[str]) -> List[Label]:
def get_text(tags: dict[str, Any], processed: set[str]) -> list[Label]:
"""
Get text representation of writable tags.
"""
texts: List[Label] = []
values: List[str] = []
texts: list[Label] = []
values: list[str] = []
if "voltage:primary" in tags:
values.append(tags["voltage:primary"])

View file

@ -7,7 +7,7 @@ import logging
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple
from typing import Optional, Tuple
import numpy as np
import svgwrite
@ -15,9 +15,9 @@ import svgwrite
from roentgen.constructor import Constructor
from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor
from roentgen.mapper import Painter
from roentgen.mapper import Map
from roentgen.osm_getter import NetworkError, get_osm
from roentgen.osm_reader import Map, OSMReader
from roentgen.osm_reader import OSMData, OSMReader
from roentgen.raster import rasterize
from roentgen.scheme import Scheme
from roentgen.ui import BoundaryBox
@ -34,7 +34,7 @@ class Tiles:
Collection of tiles.
"""
tiles: List["Tile"]
tiles: list["Tile"]
tile_1: "Tile"
tile_2: "Tile"
scale: int
@ -43,7 +43,7 @@ class Tiles:
@classmethod
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
"""Create minimal set of tiles that cover boundary box."""
tiles: List["Tile"] = []
tiles: list["Tile"] = []
tile_1 = Tile.from_coordinates(boundary_box.get_left_top(), scale)
tile_2 = Tile.from_coordinates(boundary_box.get_right_bottom(), scale)
@ -69,15 +69,16 @@ class Tiles:
:param directory: directory for tiles
:param cache_path: directory for temporary OSM files
"""
get_osm(self.boundary_box, cache_path)
map_ = OSMReader().parse_osm_file(
cache_path / (self.boundary_box.get_format() + ".osm")
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
for tile in self.tiles:
file_path: Path = tile.get_file_name(directory)
if not file_path.exists():
tile.draw_for_map(map_, directory)
tile.draw_for_map(osm_data, directory)
else:
logging.info(f"File {file_path} already exists.")
@ -97,14 +98,12 @@ class Tiles:
self.boundary_box.get_format() + ".svg"
)
if not output_path.exists():
content = get_osm(self.boundary_box, cache_path)
if not content:
logging.error("Cannot download OSM data.")
return None
map_: Map = OSMReader().parse_osm_file(
cache_path / (self.boundary_box.get_format() + ".osm")
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
lat_2, lon_1 = self.tile_1.get_coordinates()
lat_1, lon_2 = Tile(
self.tile_2.x + 1, self.tile_2.y + 1, self.scale
@ -113,26 +112,20 @@ class Tiles:
max_ = np.array((lat_2, lon_2))
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
icon_extractor: ShapeExtractor = ShapeExtractor(
extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
map_, flinger, scheme, icon_extractor
osm_data, flinger, scheme, extractor
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size
)
painter: Painter = Painter(
map_=map_,
flinger=flinger,
svg=svg,
icon_extractor=icon_extractor,
scheme=scheme,
)
painter.draw(constructor)
map_: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...")
with output_path.open("w+") as output_file:
@ -204,18 +197,18 @@ class Tile:
point_1[1], point_2[0], point_2[1], point_1[0]
).round()
def load_map(self, cache_path: Path) -> Map:
def load_osm_data(self, cache_path: Path) -> OSMData:
"""
Construct map data from extended boundary box.
:param cache_path: directory to store OSM data files
"""
boundary_box: BoundaryBox = self.get_extended_boundary_box()
get_osm(boundary_box, cache_path)
return OSMReader().parse_osm_file(
cache_path / f"{boundary_box.get_format()}.osm"
cache_file_path: Path = (
cache_path / f"{self.get_extended_boundary_box().get_format()}.osm"
)
get_osm(self.get_extended_boundary_box(), cache_file_path)
return OSMReader().parse_osm_file(cache_file_path)
def get_file_name(self, directory_name: Path) -> Path:
"""
@ -239,13 +232,13 @@ class Tile:
:param cache_path: directory to store SVG and PNG tiles
"""
try:
map_: Map = self.load_map(cache_path)
osm_data: OSMData = self.load_osm_data(cache_path)
except NetworkError as e:
raise NetworkError(f"Map does not loaded. {e.message}")
self.draw_for_map(map_, directory_name)
self.draw_for_map(osm_data, directory_name)
def draw_for_map(self, map_: Map, directory_name: Path) -> None:
def draw_for_map(self, osm_data: OSMData, directory_name: Path) -> None:
"""Draw tile using existing map."""
lat1, lon1 = self.get_coordinates()
lat2, lon2 = Tile(self.x + 1, self.y + 1, self.scale).get_coordinates()
@ -266,17 +259,11 @@ class Tile:
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
map_, flinger, scheme, icon_extractor
osm_data, flinger, scheme, icon_extractor
)
constructor.construct()
painter: Painter = Painter(
map_=map_,
flinger=flinger,
svg=svg,
icon_extractor=icon_extractor,
scheme=scheme,
)
painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
painter.draw(constructor)
logging.info(f"Writing output SVG {output_file_name}...")
@ -291,7 +278,7 @@ def ui(options) -> None:
directory: Path = workspace.get_tile_path()
if options.coordinates:
coordinates: List[float] = list(
coordinates: list[float] = list(
map(float, options.coordinates.strip().split(","))
)
tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale)

View file

@ -13,50 +13,48 @@ from dataclasses import dataclass
import numpy as np
from roentgen.osm_reader import STAGES_OF_DECAY
BOXES: str = " ▏▎▍▌▋▊▉"
BOXES_LENGTH: int = len(BOXES)
AUTHOR_MODE: str = "author"
TIME_MODE: str = "time"
LATITUDE_MAX_DIFFERENCE: float = 0.5
LONGITUDE_MAX_DIFFERENCE: float = 0.5
def parse_options(args) -> argparse.Namespace:
"""
Parse Röntgen command-line options.
"""
parser = argparse.ArgumentParser(
"""Parse Röntgen command-line options."""
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="Röntgen. OpenStreetMap renderer with custom icon set"
)
subparser = parser.add_subparsers(dest="command")
render = subparser.add_parser("render")
subparser.add_parser("icons")
mapcss = subparser.add_parser("mapcss")
subparser.add_parser("taginfo")
tile = subparser.add_parser("tile")
element = subparser.add_parser("element")
server = subparser.add_parser("server")
add_render_arguments(subparser.add_parser("render"))
add_tile_arguments(subparser.add_parser("tile"))
add_server_arguments(subparser.add_parser("server"))
add_element_arguments(subparser.add_parser("element"))
add_mapcss_arguments(subparser.add_parser("mapcss"))
add_render_arguments(render)
add_tile_arguments(tile)
add_server_arguments(server)
add_element_arguments(element)
add_mapcss_arguments(mapcss)
subparser.add_parser("icons")
subparser.add_parser("taginfo")
arguments: argparse.Namespace = parser.parse_args(args[1:])
return arguments
def add_tile_arguments(tile) -> None:
def add_tile_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for tile command."""
tile.add_argument(
parser.add_argument(
"-c",
"--coordinates",
metavar="<latitude>,<longitude>",
help="coordinates of any location inside the tile",
)
tile.add_argument(
parser.add_argument(
"-s",
"--scale",
type=int,
@ -64,19 +62,19 @@ def add_tile_arguments(tile) -> None:
help="OSM zoom level",
default=18,
)
tile.add_argument(
parser.add_argument(
"-t",
"--tile",
metavar="<scale>/<x>/<y>",
help="tile specification",
)
tile.add_argument(
parser.add_argument(
"--cache",
help="path for temporary OSM files",
default="cache",
metavar="<path>",
)
tile.add_argument(
parser.add_argument(
"-b",
"--boundary-box",
help="construct the minimum amount of tiles that cover requested "
@ -85,9 +83,9 @@ def add_tile_arguments(tile) -> None:
)
def add_server_arguments(tile) -> None:
def add_server_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for server command."""
tile.add_argument(
parser.add_argument(
"--cache",
help="path for temporary OSM files",
default="cache",
@ -95,16 +93,16 @@ def add_server_arguments(tile) -> None:
)
def add_element_arguments(element) -> None:
def add_element_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for element command."""
element.add_argument("-n", "--node")
element.add_argument("-w", "--way")
element.add_argument("-r", "--relation")
parser.add_argument("-n", "--node")
parser.add_argument("-w", "--way")
parser.add_argument("-r", "--relation")
def add_render_arguments(render) -> None:
def add_render_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for render command."""
render.add_argument(
parser.add_argument(
"-i",
"--input",
dest="input_file_name",
@ -113,7 +111,7 @@ def add_render_arguments(render) -> None:
help="input XML file name or names (if not specified, file will be "
"downloaded using OpenStreetMap API)",
)
render.add_argument(
parser.add_argument(
"-o",
"--output",
dest="output_file_name",
@ -121,14 +119,14 @@ def add_render_arguments(render) -> None:
default="out/map.svg",
help="output SVG file name",
)
render.add_argument(
parser.add_argument(
"-b",
"--boundary-box",
metavar="<lon1>,<lat1>,<lon2>,<lat2>",
help='geo boundary box, use space before "-" if the first value is '
"negative",
)
render.add_argument(
parser.add_argument(
"-s",
"--scale",
metavar="<float>",
@ -136,61 +134,63 @@ def add_render_arguments(render) -> None:
default=18,
type=float,
)
render.add_argument(
parser.add_argument(
"--cache",
help="path for temporary OSM files",
default="cache",
metavar="<path>",
)
render.add_argument(
parser.add_argument(
"--labels",
help="label drawing mode: `no`, `main`, or `all`",
dest="label_mode",
default="main",
)
render.add_argument(
parser.add_argument(
"--overlap",
dest="overlap",
default=12,
type=int,
help="how many pixels should be left around icons and text",
)
render.add_argument(
parser.add_argument(
"--mode",
default="normal",
help="map drawing mode",
)
render.add_argument(
parser.add_argument(
"--seed",
default="",
help="seed for random",
)
render.add_argument(
parser.add_argument(
"--level",
default=None,
help="display only this floor level",
)
def add_mapcss_arguments(mapcss) -> None:
def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for mapcss command."""
mapcss.add_argument(
parser.add_argument(
"--icons",
action=argparse.BooleanOptionalAction,
default=True,
help="add icons for nodes and areas",
)
mapcss.add_argument(
parser.add_argument(
"--ways",
action=argparse.BooleanOptionalAction,
default=True,
help="add style for ways and relations",
)
mapcss.add_argument(
parser.add_argument(
"--lifecycle",
action=argparse.BooleanOptionalAction,
default=True,
help="add icons for lifecycle tags",
help="add icons for lifecycle tags; be careful: this will increase the "
f"number of node and area selectors by {len(STAGES_OF_DECAY) + 1} "
f"times",
)
@ -245,6 +245,8 @@ class BoundaryBox:
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2> or simply
<left>,<bottom>,<right>,<top>.
"""
boundary_box = boundary_box.replace(" ", "")
matcher = re.match(
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
@ -256,10 +258,10 @@ class BoundaryBox:
return None
try:
left = float(matcher.group("left"))
bottom = float(matcher.group("bottom"))
right = float(matcher.group("right"))
top = float(matcher.group("top"))
left: float = float(matcher.group("left"))
bottom: float = float(matcher.group("bottom"))
right: float = float(matcher.group("right"))
top: float = float(matcher.group("top"))
except ValueError:
logging.fatal("Invalid boundary box.")
return None
@ -270,7 +272,10 @@ class BoundaryBox:
if bottom >= top:
logging.error("Negative vertical boundary.")
return None
if right - left > 0.5 or top - bottom > 0.5:
if (
right - left > LONGITUDE_MAX_DIFFERENCE
or top - bottom > LATITUDE_MAX_DIFFERENCE
):
logging.error("Boundary box is too big.")
return None
@ -285,9 +290,7 @@ class BoundaryBox:
return self.bottom, self.right
def round(self) -> "BoundaryBox":
"""
Round boundary box.
"""
"""Round boundary box."""
self.left = round(self.left * 1000) / 1000 - 0.001
self.bottom = round(self.bottom * 1000) / 1000 - 0.001
self.right = round(self.right * 1000) / 1000 + 0.001

View file

@ -18,28 +18,20 @@ class MinMax:
max_: Any = None
def update(self, value: Any) -> None:
"""
Update minimum and maximum with new value.
"""
"""Update minimum and maximum with new value."""
self.min_ = value if not self.min_ or value < self.min_ else self.min_
self.max_ = value if not self.max_ or value > self.max_ else self.max_
def delta(self) -> Any:
"""
Difference between maximum and minimum.
"""
"""Difference between maximum and minimum."""
return self.max_ - self.min_
def center(self) -> Any:
"""
Get middle point between minimum and maximum.
"""
"""Get middle point between minimum and maximum."""
return (self.min_ + self.max_) / 2
def is_empty(self) -> bool:
"""
Check if interval is empty.
"""
"""Check if interval is empty."""
return self.min_ == self.max_
def __repr__(self) -> str:

View file

@ -12,12 +12,15 @@ def compute_angle(vector: np.array):
For the given vector compute an angle between it and (1, 0) vector. The
result is in [0, 2π].
"""
if vector[0] == 0:
if vector[1] > 0:
return np.pi / 2
return np.pi + np.pi / 2
if vector[0] < 0:
return np.arctan(vector[1] / vector[0]) + np.pi
if vector[1] < 0:
return np.arctan(vector[1] / vector[0]) + 2 * np.pi
else:
return np.arctan(vector[1] / vector[0])
return np.arctan(vector[1] / vector[0])
def turn_by_angle(vector: np.array, angle: float):

View file

@ -15,6 +15,10 @@ def check_and_create(directory: Path) -> Path:
class Workspace:
"""
Project file and directory paths and generated files and directories.
"""
# Project directories and files, that are the part of the repository.
SCHEME_PATH: Path = Path("scheme")

View file

@ -8,9 +8,10 @@ __email__ = "me@enzet.ru"
def test_round_zero_coordinates() -> None:
box: BoundaryBox = BoundaryBox(0, 0, 0, 0).round()
assert box.get_format() == "-0.001,-0.001,0.001,0.001"
assert (
BoundaryBox(0, 0, 0, 0).round().get_format()
== "-0.001,-0.001,0.001,0.001"
)
def test_round_coordinates() -> None:

View file

@ -11,9 +11,7 @@ __email__ = "me@enzet.ru"
def test_is_bright() -> None:
"""
Test detecting color brightness.
"""
"""Test detecting color brightness."""
assert is_bright(Color("white"))
assert is_bright(Color("yellow"))
assert not is_bright(Color("brown"))
@ -21,9 +19,7 @@ def test_is_bright() -> None:
def test_gradient() -> None:
"""
Test color picking from gradient.
"""
"""Test color picking from gradient."""
color: Color = get_gradient_color(
0.5, MinMax(0, 1), [Color("black"), Color("white")]
)

View file

@ -10,9 +10,7 @@ __email__ = "me@enzet.ru"
def test_pseudo_mercator() -> None:
"""
Test pseudo-Mercator projection.
"""
"""Test pseudo-Mercator projection."""
assert np.allclose(pseudo_mercator(np.array((0, 0))), np.array((0, 0)))
assert np.allclose(pseudo_mercator(np.array((0, 10))), np.array((10, 0)))
assert np.allclose(
@ -21,9 +19,7 @@ def test_pseudo_mercator() -> None:
def test_osm_zoom_level_to_pixels_per_meter() -> None:
"""
Test scale computation.
"""
"""Test scale computation."""
assert np.allclose(
osm_zoom_level_to_pixels_per_meter(18), 1.6759517949045808
)

View file

@ -1,8 +1,6 @@
"""
Test icon generation for nodes.
"""
from typing import Dict, Set
import pytest
from roentgen.grid import IconCollection
@ -34,11 +32,9 @@ def test_icons_by_name(init_collection) -> None:
init_collection.draw_icons(workspace.get_icons_by_name_path(), by_name=True)
def get_icon(tags: Dict[str, str]) -> IconSet:
"""
Construct icon from tags.
"""
processed: Set[str] = set()
def get_icon(tags: dict[str, str]) -> IconSet:
"""Construct icon from tags."""
processed: set[str] = set()
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed)
return icon

View file

@ -1,8 +1,6 @@
"""
Test label generation for nodes.
"""
from typing import List, Set
from roentgen.text import Label
from test import SCHEME
@ -10,18 +8,14 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
def construct_labels(tags) -> List[Label]:
"""
Construct labels from OSM node tags.
"""
processed: Set[str] = set()
def construct_labels(tags) -> list[Label]:
"""Construct labels from OSM node tags."""
processed: set[str] = set()
return SCHEME.construct_text(tags, "all", processed)
def test_1_label() -> None:
"""
Test tags that should be converted into single label.
"""
"""Test tags that should be converted into single label."""
labels = construct_labels({"name": "Name"})
assert len(labels) == 1
assert labels[0].text == "Name"
@ -37,9 +31,7 @@ def test_1_label_unknown_tags() -> None:
def test_2_labels() -> None:
"""
Test tags that should be converted into two labels.
"""
"""Test tags that should be converted into two labels."""
labels = construct_labels({"name": "Name", "ref": "5"})
assert len(labels) == 2
assert labels[0].text == "Name"

View file

@ -10,9 +10,7 @@ __email__ = "me@enzet.ru"
def test_mapcss() -> None:
"""
Test MapCSS generation.
"""
"""Test MapCSS generation."""
writer: MapCSSWriter = MapCSSWriter(SCHEME, "icons")
matcher: NodeMatcher = NodeMatcher(
{"tags": {"natural": "tree"}, "shapes": ["tree"]}

View file

@ -3,35 +3,31 @@ Test OSM XML parsing.
"""
import numpy as np
from roentgen.osm_reader import OSMNode, OSMReader, OSMRelation, OSMWay
from roentgen.osm_reader import OSMNode, OSMReader, OSMRelation, OSMWay, OSMData
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
def test_node() -> None:
"""
Test OSM node parsing from XML.
"""
reader = OSMReader()
map_ = reader.parse_osm_text(
"""Test OSM node parsing from XML."""
reader: OSMReader = OSMReader()
osm_data: OSMData = reader.parse_osm_text(
"""<?xml version="1.0"?>
<osm>
<node id="42" lon="5" lat="10" />
</osm>"""
)
assert 42 in map_.nodes
node: OSMNode = map_.nodes[42]
assert 42 in osm_data.nodes
node: OSMNode = osm_data.nodes[42]
assert node.id_ == 42
assert np.allclose(node.coordinates, np.array([10, 5]))
def test_node_with_tag() -> None:
"""
Test OSM node parsing from XML.
"""
"""Test OSM node parsing from XML."""
reader = OSMReader()
map_ = reader.parse_osm_text(
osm_data: OSMData = reader.parse_osm_text(
"""<?xml version="1.0"?>
<osm>
<node id="42" lon="5" lat="10">
@ -39,35 +35,31 @@ def test_node_with_tag() -> None:
</node>
</osm>"""
)
assert 42 in map_.nodes
node: OSMNode = map_.nodes[42]
assert 42 in osm_data.nodes
node: OSMNode = osm_data.nodes[42]
assert node.id_ == 42
assert np.allclose(node.coordinates, np.array([10, 5]))
assert node.tags["key"] == "value"
def test_way() -> None:
"""
Test OSM way parsing from XML.
"""
reader = OSMReader()
map_ = reader.parse_osm_text(
"""Test OSM way parsing from XML."""
reader: OSMReader = OSMReader()
osm_data: OSMData = reader.parse_osm_text(
"""<?xml version="1.0"?>
<osm>
<way id="42" />
</osm>"""
)
assert 42 in map_.ways
way: OSMWay = map_.ways[42]
assert 42 in osm_data.ways
way: OSMWay = osm_data.ways[42]
assert way.id_ == 42
def test_nodes() -> None:
"""
Test OSM node parsing from XML.
"""
"""Test OSM node parsing from XML."""
reader = OSMReader()
map_ = reader.parse_osm_text(
osm_data: OSMData = reader.parse_osm_text(
"""<?xml version="1.0"?>
<osm>
<node id="1" lon="5" lat="10" />
@ -77,18 +69,16 @@ def test_nodes() -> None:
</way>
</osm>"""
)
way: OSMWay = map_.ways[2]
way: OSMWay = osm_data.ways[2]
assert len(way.nodes) == 1
assert way.nodes[0].id_ == 1
assert way.tags["key"] == "value"
def test_relation() -> None:
"""
Test OSM node parsing from XML.
"""
reader = OSMReader()
map_ = reader.parse_osm_text(
"""Test OSM node parsing from XML."""
reader: OSMReader = OSMReader()
osm_data: OSMData = reader.parse_osm_text(
"""<?xml version="1.0"?>
<osm>
<node id="1" lon="5" lat="10" />
@ -101,8 +91,8 @@ def test_relation() -> None:
</relation>
</osm>"""
)
assert 3 in map_.relations
relation: OSMRelation = map_.relations[3]
assert 3 in osm_data.relations
relation: OSMRelation = osm_data.relations[3]
assert relation.id_ == 3
assert relation.tags["key"] == "value"
assert len(relation.members) == 1

View file

@ -8,23 +8,17 @@ __email__ = "me@enzet.ru"
def test_style_empty() -> None:
"""
Test constructing style of empty tags.
"""
"""Test constructing style of empty tags."""
assert SCHEME.get_style({}, 18) == []
def test_style_unknown() -> None:
"""
Test constructing style of unknown tags.
"""
"""Test constructing style of unknown tags."""
assert SCHEME.get_style({"aaa": "bbb"}, 18) == []
def test_style_area() -> None:
"""
Test constructing style of landuse=grass.
"""
"""Test constructing style of landuse=grass."""
style = SCHEME.get_style({"landuse": "grass"}, 18)
assert len(style) == 1
assert style[0].style == {"fill": "#CFE0A8", "stroke": "#BFD098"}

View file

@ -10,18 +10,14 @@ __email__ = "me@enzet.ru"
def check_length(value: str, expected: Optional[float]) -> None:
"""
Assert that constructed value is equals to an expected one.
"""
"""Assert that constructed value is equals to an expected one."""
tagged = Tagged()
tagged.tags["a"] = value
assert tagged.get_length("a") == expected
def test_meters() -> None:
"""
Test length in meters processing.
"""
"""Test length in meters processing."""
check_length("50m", 50.0)
check_length("50.m", 50.0)
check_length("50.05m", 50.05)
@ -31,16 +27,12 @@ def test_meters() -> None:
def test_kilometers() -> None:
"""
Test length in meters processing.
"""
"""Test length in meters processing."""
check_length("50km", 50_000.0)
check_length("50 km", 50_000.0)
def test_miles() -> None:
"""
Test length in meters processing.
"""
"""Test length in meters processing."""
check_length("1mi", 1609.344)
check_length("50 mi", 50 * 1609.344)

View file

@ -8,8 +8,6 @@ __email__ = "me@enzet.ru"
def test_voltage() -> None:
"""
Test voltage tag value processing.
"""
"""Test voltage tag value processing."""
assert format_voltage("42") == "42 V"
assert format_voltage("42000") == "42 kV"