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

View file

@ -37,6 +37,7 @@ def get_gradient_color(
range_coefficient: float = ( range_coefficient: float = (
0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta() 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)) range_coefficient = min(1.0, max(0.0, range_coefficient))
index: int = int(range_coefficient * color_length) index: int = int(range_coefficient * color_length)
coefficient: float = ( coefficient: float = (

View file

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

View file

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

View file

@ -4,7 +4,11 @@ Figures displayed on the map.
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import numpy as np 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.flinger import Flinger
from roentgen.osm_reader import OSMNode, Tagged from roentgen.osm_reader import OSMNode, Tagged
from roentgen.road import Lane from roentgen.road import Lane
@ -97,14 +101,85 @@ class Building(Figure):
if levels: if levels:
self.min_height = float(levels) * 2.5 self.min_height = float(levels) * 2.5
height: Optional[str] = self.get_length("height") height: Optional[float] = self.get_length("height")
if height: if height:
self.height = height self.height = height
height: Optional[str] = self.get_length("min_height") height: Optional[float] = self.get_length("min_height")
if height: if height:
self.min_height = 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): class StyledFigure(Figure):
""" """
@ -161,14 +236,110 @@ class Road(Figure):
pass 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: class Segment:
""" """
Line segment. Line segment.
""" """
def __init__(self, point_1: np.array, point_2: np.array): def __init__(self, point_1: np.array, point_2: np.array):
self.point_1 = point_1 self.point_1: np.array = point_1
self.point_2 = point_2 self.point_2: np.array = point_2
difference: np.array = point_2 - point_1 difference: np.array = point_2 - point_1
vector: np.array = difference / np.linalg.norm(difference) 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: 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 = "" path: str = ""
prev_node: Optional[OSMNode] = None prev_node: Optional[OSMNode] = None
for node in nodes: for node in nodes:

View file

@ -27,10 +27,13 @@ def pseudo_mercator(coordinates: np.array) -> np.array:
return np.array((coordinates[1], y)) 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) Convert OSM zoom level to pixels per meter on Equator. See
to pixels per meter on Equator. 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 return 2 ** zoom_level / 156415

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
""" """
Simple OpenStreetMap renderer. Simple OpenStreetMap renderer.
""" """
from typing import Any, Dict, Iterator, Set from typing import Any, Iterator
import numpy as np import numpy as np
import svgwrite import svgwrite
@ -12,33 +12,27 @@ from svgwrite.shapes import Rect
from roentgen import ui from roentgen import ui
from roentgen.constructor import Constructor from roentgen.constructor import Constructor
from roentgen.direction import DirectionSet, Sector
from roentgen.figure import Road from roentgen.figure import Road
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor from roentgen.osm_reader import OSMNode
from roentgen.osm_reader import Map, OSMNode
from roentgen.point import Occupied from roentgen.point import Occupied
from roentgen.road import Intersection, RoadPart from roentgen.road import Intersection, RoadPart
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.ui import AUTHOR_MODE, TIME_MODE
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
AUTHOR_MODE = "user-coloring"
CREATION_TIME_MODE = "time"
class Map:
class Painter:
""" """
Map drawing. Map drawing.
""" """
def __init__( def __init__(
self, self,
map_: Map,
flinger: Flinger, flinger: Flinger,
svg: svgwrite.Drawing, svg: svgwrite.Drawing,
icon_extractor: ShapeExtractor,
scheme: Scheme, scheme: Scheme,
overlap: int = 12, overlap: int = 12,
mode: str = "normal", mode: str = "normal",
@ -48,20 +42,16 @@ class Painter:
self.mode: str = mode self.mode: str = mode
self.label_mode: str = label_mode self.label_mode: str = label_mode
self.map_: Map = map_
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg self.svg: svgwrite.Drawing = svg
self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.background_color: Color = self.scheme.get_color("background_color") 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") self.background_color: Color = Color("#111111")
def draw(self, constructor: Constructor) -> None: def draw(self, constructor: Constructor) -> None:
""" """Draw map."""
Draw map.
"""
self.svg.add( self.svg.add(
Rect((0, 0), self.flinger.size, fill=self.background_color) Rect((0, 0), self.flinger.size, fill=self.background_color)
) )
@ -84,9 +74,13 @@ class Painter:
for road in roads: for road in roads:
self.draw_road(road, road.matcher.color) 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_buildings(constructor)
self.draw_direction(constructor)
# All other points # All other points
@ -101,10 +95,6 @@ class Painter:
steps: int = len(nodes) steps: int = len(nodes)
for index, node in enumerate(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( ui.progress_bar(
index, steps * 3, step=10, text="Drawing main icons" 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" steps * 2 + index, steps * 3, step=10, text="Drawing texts"
) )
if ( if (
self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE] self.mode not in [TIME_MODE, AUTHOR_MODE]
and self.label_mode != "no" and self.label_mode != "no"
): ):
point.draw_texts(self.svg, occupied, self.label_mode) point.draw_texts(self.svg, occupied, self.label_mode)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes") 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: def draw_buildings(self, constructor: Constructor) -> None:
""" """Draw buildings: shade, walls, and roof."""
Draw buildings: shade, walls, and roof.
"""
# Draw shade.
building_shade: Group = Group(opacity=0.1) building_shade: Group = Group(opacity=0.1)
scale: float = self.flinger.get_scale() / 3.0 scale: float = self.flinger.get_scale() / 3.0
for building in constructor.buildings: for building in constructor.buildings:
shift_1 = np.array((scale * building.min_height, 0)) building.draw_shade(building_shade, self.flinger)
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)
self.svg.add(building_shade) self.svg.add(building_shade)
# Draw buildings.
previous_height: float = 0 previous_height: float = 0
count: int = len(constructor.heights) count: int = len(constructor.heights)
for index, height in enumerate(sorted(constructor.heights)): for index, height in enumerate(sorted(constructor.heights)):
ui.progress_bar(index, count, step=1, text="Drawing buildings") ui.progress_bar(index, count, step=1, text="Drawing buildings")
fill: Color() fill: Color()
for way in constructor.buildings: for building in constructor.buildings:
if way.height < height or way.min_height > height: if building.height < height or building.min_height > height:
continue continue
shift_1 = [0, -previous_height * scale] building.draw_walls(self.svg, height, 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))
command = ( for building in constructor.buildings:
"M", if building.height == height:
segment.point_1 + shift_1, building.draw_roof(self.svg, self.flinger, scale)
"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)
previous_height = height previous_height = height
ui.progress_bar(-1, count, step=1, text="Drawing buildings") 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( def draw_road(
self, road: Road, color: Color, extra_width: float = 0 self, road: Road, color: Color, extra_width: float = 0
) -> None: ) -> None:
@ -336,7 +158,7 @@ class Painter:
scale = self.flinger.get_scale(road.outers[0][0].coordinates) scale = self.flinger.get_scale(road.outers[0][0].coordinates)
path_commands: str = road.get_path(self.flinger) path_commands: str = road.get_path(self.flinger)
path = Path(d=path_commands) path = Path(d=path_commands)
style: Dict[str, Any] = { style: dict[str, Any] = {
"fill": "none", "fill": "none",
"stroke": color.hex, "stroke": color.hex,
"stroke-linecap": "round", "stroke-linecap": "round",
@ -350,7 +172,7 @@ class Painter:
""" """
Draw road as simple SVG path. Draw road as simple SVG path.
""" """
nodes: Dict[OSMNode, Set[RoadPart]] = {} nodes: dict[OSMNode, set[RoadPart]] = {}
for road in roads: for road in roads:
for index in range(len(road.outers[0]) - 1): for index in range(len(road.outers[0]) - 1):
@ -379,7 +201,7 @@ class Painter:
intersection.draw(self.svg, scale, True) 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. 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 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. Check if element described by tags is overground.
""" """

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import logging
import os import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import List
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __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." 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-png", to_.absolute()]
commands += ["--export-dpi", str(dpi)] commands += ["--export-dpi", str(dpi)]
if area: if area:

View file

@ -2,7 +2,7 @@
WIP: road shape drawing. WIP: road shape drawing.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional from typing import Optional
import numpy as np import numpy as np
import svgwrite import svgwrite
@ -48,7 +48,7 @@ class RoadPart:
self, self,
point_1: np.array, point_1: np.array,
point_2: np.array, point_2: np.array,
lanes: List[Lane], lanes: list[Lane],
scale: False, scale: False,
): ):
""" """
@ -58,7 +58,7 @@ class RoadPart:
""" """
self.point_1: np.array = point_1 self.point_1: np.array = point_1
self.point_2: np.array = point_2 self.point_2: np.array = point_2
self.lanes: List[Lane] = lanes self.lanes: list[Lane] = lanes
if lanes: if lanes:
self.width = sum(map(lambda x: x.get_width(scale), lanes)) self.width = sum(map(lambda x: x.get_width(scale), lanes))
else: else:
@ -297,8 +297,8 @@ class Intersection:
points of the road parts should be the same. points of the road parts should be the same.
""" """
def __init__(self, parts: List[RoadPart]): def __init__(self, parts: list[RoadPart]):
self.parts: List[RoadPart] = sorted(parts, key=lambda x: x.get_angle()) self.parts: list[RoadPart] = sorted(parts, key=lambda x: x.get_angle())
for index in range(len(self.parts)): for index in range(len(self.parts)):
next_index: int = 0 if index == len(self.parts) - 1 else index + 1 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 dataclasses import dataclass
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union from typing import Any, Optional, Tuple, Union
import yaml import yaml
from colour import Color from colour import Color
@ -30,7 +30,7 @@ class LineStyle:
SVG line style and its priority. SVG line style and its priority.
""" """
style: Dict[str, Union[int, float, str]] style: dict[str, Union[int, float, str]]
priority: float = 0.0 priority: float = 0.0
@ -48,7 +48,7 @@ class MatchingType(Enum):
def is_matched_tag( def is_matched_tag(
matcher_tag_key: str, matcher_tag_key: str,
matcher_tag_value: Union[str, list], matcher_tag_value: Union[str, list],
tags: Dict[str, str], tags: dict[str, str],
) -> MatchingType: ) -> MatchingType:
""" """
Check whether element tags contradict tag matcher. Check whether element tags contradict tag matcher.
@ -89,10 +89,10 @@ class Matcher:
Tag matching. Tag matching.
""" """
def __init__(self, structure: Dict[str, Any]): def __init__(self, structure: dict[str, Any]):
self.tags: Dict[str, str] = structure["tags"] self.tags: dict[str, str] = structure["tags"]
self.exception: Dict[str, str] = {} self.exception: dict[str, str] = {}
if "exception" in structure: if "exception" in structure:
self.exception = structure["exception"] self.exception = structure["exception"]
@ -100,11 +100,11 @@ class Matcher:
if "replace_shapes" in structure: if "replace_shapes" in structure:
self.replace_shapes = structure["replace_shapes"] self.replace_shapes = structure["replace_shapes"]
self.location_restrictions: Dict[str, str] = {} self.location_restrictions: dict[str, str] = {}
if "location_restrictions" in structure: if "location_restrictions" in structure:
self.location_restrictions = structure["location_restrictions"] 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. Check whether element tags matches tag matcher.
@ -157,7 +157,7 @@ class NodeMatcher(Matcher):
Tag specification 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 "*" # Dictionary with tag keys and values, value lists, or "*"
super().__init__(structure) super().__init__(structure)
@ -200,11 +200,11 @@ class WayMatcher(Matcher):
Special tag matcher for ways. 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) super().__init__(structure)
self.style: Dict[str, Any] = {"fill": "none"} self.style: dict[str, Any] = {"fill": "none"}
if "style" in structure: if "style" in structure:
style: Dict[str, Any] = structure["style"] style: dict[str, Any] = structure["style"]
for key in style: for key in style:
if str(style[key]).endswith("_color"): if str(style[key]).endswith("_color"):
self.style[key] = scheme.get_color(style[key]).hex.upper() self.style[key] = scheme.get_color(style[key]).hex.upper()
@ -223,7 +223,7 @@ class RoadMatcher(Matcher):
Special tag matcher for highways. 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) super().__init__(structure)
self.border_color: Color = Color( self.border_color: Color = Color(
scheme.get_color(structure["border_color"]) scheme.get_color(structure["border_color"])
@ -250,33 +250,33 @@ class Scheme:
specification specification
""" """
with file_name.open() as input_file: 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 input_file.read(), Loader=yaml.FullLoader
) )
self.node_matchers: List[NodeMatcher] = [] self.node_matchers: list[NodeMatcher] = []
for group in content["node_icons"]: for group in content["node_icons"]:
for element in group["tags"]: for element in group["tags"]:
self.node_matchers.append(NodeMatcher(element)) self.node_matchers.append(NodeMatcher(element))
self.colors: Dict[str, str] = content["colors"] self.colors: dict[str, str] = content["colors"]
self.material_colors: Dict[str, str] = content["material_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"] 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"] 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"] Matcher(x) for x in content["area_tags"]
] ]
self.tags_to_write: List[str] = content["tags_to_write"] self.tags_to_write: list[str] = content["tags_to_write"]
self.prefix_to_write: List[str] = content["prefix_to_write"] self.prefix_to_write: list[str] = content["prefix_to_write"]
self.tags_to_skip: List[str] = content["tags_to_skip"] self.tags_to_skip: list[str] = content["tags_to_skip"]
self.prefix_to_skip: List[str] = content["prefix_to_skip"] self.prefix_to_skip: list[str] = content["prefix_to_skip"]
# Storage for created icon sets. # 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: def get_color(self, color: str) -> Color:
""" """
@ -327,8 +327,8 @@ class Scheme:
def get_icon( def get_icon(
self, self,
extractor: ShapeExtractor, extractor: ShapeExtractor,
tags: Dict[str, Any], tags: dict[str, Any],
processed: Set[str], processed: set[str],
for_: str = "node", for_: str = "node",
) -> Tuple[IconSet, int]: ) -> Tuple[IconSet, int]:
""" """
@ -347,7 +347,7 @@ class Scheme:
return self.cache[tags_hash] return self.cache[tags_hash]
main_icon: Optional[Icon] = None main_icon: Optional[Icon] = None
extra_icons: List[Icon] = [] extra_icons: list[Icon] = []
priority: int = 0 priority: int = 0
index: int = 0 index: int = 0
@ -358,7 +358,7 @@ class Scheme:
matched: bool = matcher.is_matched(tags) matched: bool = matcher.is_matched(tags)
if not matched: if not matched:
continue continue
matcher_tags: Set[str] = set(matcher.tags.keys()) matcher_tags: set[str] = set(matcher.tags.keys())
priority = len(self.node_matchers) - index priority = len(self.node_matchers) - index
if not matcher.draw: if not matcher.draw:
processed |= matcher_tags processed |= matcher_tags
@ -437,7 +437,7 @@ class Scheme:
return returned, priority 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. Get line style based on tags and scale.
""" """
@ -451,7 +451,7 @@ class Scheme:
return line_styles 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: for matcher in self.road_matchers:
if not matcher.is_matched(tags): if not matcher.is_matched(tags):
continue continue
@ -459,12 +459,12 @@ class Scheme:
return None return None
def construct_text( def construct_text(
self, tags: Dict[str, str], draw_captions: str, processed: Set[str] self, tags: dict[str, str], draw_captions: str, processed: set[str]
) -> List[Label]: ) -> list[Label]:
""" """
Construct labels for not processed tags. Construct labels for not processed tags.
""" """
texts: List[Label] = [] texts: list[Label] = []
name = None name = None
alt_name = None alt_name = None
@ -490,7 +490,7 @@ class Scheme:
alt_name = "" alt_name = ""
alt_name += "ex " + tags["old_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: if name:
texts.append(Label(name, Color("black"))) texts.append(Label(name, Color("black")))
@ -535,7 +535,7 @@ class Scheme:
texts.append(Label(tags[tag])) texts.append(Label(tags[tag]))
return texts 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. Check whether way described by tags is area.
""" """
@ -545,7 +545,7 @@ class Scheme:
return False return False
def process_ignored( def process_ignored(
self, tags: Dict[str, str], processed: Set[str] self, tags: dict[str, str], processed: set[str]
) -> None: ) -> None:
""" """
Mark all ignored tag as processed. Mark all ignored tag as processed.
@ -553,6 +553,4 @@ class Scheme:
:param tags: input tag dictionary :param tags: input tag dictionary
:param processed: processed set :param processed: processed set
""" """
for tag in tags: [processed.add(tag) for tag in tags if self.is_no_drawable(tag)]
if self.is_no_drawable(tag):
processed.add(tag)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,28 +18,20 @@ class MinMax:
max_: Any = None max_: Any = None
def update(self, value: 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.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_ self.max_ = value if not self.max_ or value > self.max_ else self.max_
def delta(self) -> Any: def delta(self) -> Any:
""" """Difference between maximum and minimum."""
Difference between maximum and minimum.
"""
return self.max_ - self.min_ return self.max_ - self.min_
def center(self) -> Any: def center(self) -> Any:
""" """Get middle point between minimum and maximum."""
Get middle point between minimum and maximum.
"""
return (self.min_ + self.max_) / 2 return (self.min_ + self.max_) / 2
def is_empty(self) -> bool: def is_empty(self) -> bool:
""" """Check if interval is empty."""
Check if interval is empty.
"""
return self.min_ == self.max_ return self.min_ == self.max_
def __repr__(self) -> str: 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 For the given vector compute an angle between it and (1, 0) vector. The
result is in [0, 2π]. 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: if vector[0] < 0:
return np.arctan(vector[1] / vector[0]) + np.pi return np.arctan(vector[1] / vector[0]) + np.pi
if vector[1] < 0: if vector[1] < 0:
return np.arctan(vector[1] / vector[0]) + 2 * np.pi 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): def turn_by_angle(vector: np.array, angle: float):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,23 +8,17 @@ __email__ = "me@enzet.ru"
def test_style_empty() -> None: def test_style_empty() -> None:
""" """Test constructing style of empty tags."""
Test constructing style of empty tags.
"""
assert SCHEME.get_style({}, 18) == [] assert SCHEME.get_style({}, 18) == []
def test_style_unknown() -> None: def test_style_unknown() -> None:
""" """Test constructing style of unknown tags."""
Test constructing style of unknown tags.
"""
assert SCHEME.get_style({"aaa": "bbb"}, 18) == [] assert SCHEME.get_style({"aaa": "bbb"}, 18) == []
def test_style_area() -> None: def test_style_area() -> None:
""" """Test constructing style of landuse=grass."""
Test constructing style of landuse=grass.
"""
style = SCHEME.get_style({"landuse": "grass"}, 18) style = SCHEME.get_style({"landuse": "grass"}, 18)
assert len(style) == 1 assert len(style) == 1
assert style[0].style == {"fill": "#CFE0A8", "stroke": "#BFD098"} 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: 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 = Tagged()
tagged.tags["a"] = value tagged.tags["a"] = value
assert tagged.get_length("a") == expected assert tagged.get_length("a") == expected
def test_meters() -> None: def test_meters() -> None:
""" """Test length in meters processing."""
Test length in meters processing.
"""
check_length("50m", 50.0) check_length("50m", 50.0)
check_length("50.m", 50.0) check_length("50.m", 50.0)
check_length("50.05m", 50.05) check_length("50.05m", 50.05)
@ -31,16 +27,12 @@ def test_meters() -> None:
def test_kilometers() -> None: def test_kilometers() -> None:
""" """Test length in meters processing."""
Test length in meters processing.
"""
check_length("50km", 50_000.0) check_length("50km", 50_000.0)
check_length("50 km", 50_000.0) check_length("50 km", 50_000.0)
def test_miles() -> None: def test_miles() -> None:
""" """Test length in meters processing."""
Test length in meters processing.
"""
check_length("1mi", 1609.344) check_length("1mi", 1609.344)
check_length("50 mi", 50 * 1609.344) check_length("50 mi", 50 * 1609.344)

View file

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