mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-21 04:56:24 +02:00
Refactor figures; get use of Python 3.9 features.
This commit is contained in:
parent
053324451a
commit
3bcf026862
36 changed files with 698 additions and 750 deletions
107
roentgen.py
107
roentgen.py
|
@ -18,31 +18,32 @@ from roentgen.grid import draw_icons
|
|||
from roentgen.icon import ShapeExtractor
|
||||
from roentgen.mapper import (
|
||||
AUTHOR_MODE,
|
||||
CREATION_TIME_MODE,
|
||||
Painter,
|
||||
TIME_MODE,
|
||||
Map,
|
||||
check_level_number,
|
||||
check_level_overground,
|
||||
)
|
||||
from roentgen.osm_getter import NetworkError, get_osm
|
||||
from roentgen.osm_reader import Map, OSMReader, OverpassReader
|
||||
from roentgen.osm_reader import OSMData, OSMReader, OverpassReader
|
||||
from roentgen.point import Point
|
||||
from roentgen.scheme import LineStyle, Scheme
|
||||
from roentgen.ui import BoundaryBox, parse_options
|
||||
from roentgen.util import MinMax
|
||||
from roentgen.workspace import workspace
|
||||
from roentgen.workspace import Workspace
|
||||
|
||||
|
||||
def main(options) -> None:
|
||||
"""
|
||||
Röntgen entry point.
|
||||
|
||||
:param argv: command-line arguments
|
||||
:param options: command-line arguments
|
||||
"""
|
||||
if not options.boundary_box and not options.input_file_name:
|
||||
logging.fatal("Specify either --boundary-box, or --input.")
|
||||
exit(1)
|
||||
|
||||
if options.boundary_box:
|
||||
box: List[float] = list(
|
||||
map(float, options.boundary_box.replace(" ", "").split(","))
|
||||
)
|
||||
boundary_box = BoundaryBox(box[0], box[1], box[2], box[3])
|
||||
boundary_box: BoundaryBox = BoundaryBox.from_text(options.boundary_box)
|
||||
|
||||
cache_path: Path = Path(options.cache)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
@ -53,28 +54,35 @@ def main(options) -> None:
|
|||
input_file_names = list(map(Path, options.input_file_name))
|
||||
else:
|
||||
try:
|
||||
get_osm(boundary_box, cache_path)
|
||||
cache_file_path: Path = (
|
||||
cache_path / f"{boundary_box.get_format()}.osm"
|
||||
)
|
||||
get_osm(boundary_box, cache_file_path)
|
||||
except NetworkError as e:
|
||||
logging.fatal(e.message)
|
||||
sys.exit(1)
|
||||
input_file_names = [cache_path / f"{options.boundary_box}.osm"]
|
||||
input_file_names = [cache_file_path]
|
||||
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
min_: np.array
|
||||
max_: np.array
|
||||
map_: Map
|
||||
osm_data: OSMData
|
||||
|
||||
if input_file_names[0].name.endswith(".json"):
|
||||
reader: OverpassReader = OverpassReader()
|
||||
reader.parse_json_file(input_file_names[0])
|
||||
|
||||
map_ = reader.map_
|
||||
osm_data = reader.osm_data
|
||||
view_box = MinMax(
|
||||
np.array((map_.boundary_box[0].min_, map_.boundary_box[1].min_)),
|
||||
np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_)),
|
||||
np.array(
|
||||
(osm_data.boundary_box[0].min_, osm_data.boundary_box[1].min_)
|
||||
),
|
||||
np.array(
|
||||
(osm_data.boundary_box[0].max_, osm_data.boundary_box[1].max_)
|
||||
),
|
||||
)
|
||||
else:
|
||||
is_full: bool = options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]
|
||||
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE]
|
||||
osm_reader = OSMReader(is_full=is_full)
|
||||
|
||||
for file_name in input_file_names:
|
||||
|
@ -84,24 +92,19 @@ def main(options) -> None:
|
|||
|
||||
osm_reader.parse_osm_file(file_name)
|
||||
|
||||
map_ = osm_reader.map_
|
||||
osm_data = osm_reader.osm_data
|
||||
|
||||
if options.boundary_box:
|
||||
boundary_box: List[float] = list(
|
||||
map(float, options.boundary_box.split(","))
|
||||
)
|
||||
view_box = MinMax(
|
||||
np.array((boundary_box[1], boundary_box[0])),
|
||||
np.array((boundary_box[3], boundary_box[2])),
|
||||
np.array((boundary_box.bottom, boundary_box.left)),
|
||||
np.array((boundary_box.top, boundary_box.right)),
|
||||
)
|
||||
else:
|
||||
view_box = map_.view_box
|
||||
view_box = osm_data.view_box
|
||||
|
||||
flinger: Flinger = Flinger(view_box, options.scale)
|
||||
size: np.array = flinger.size
|
||||
|
||||
Path("out").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||
options.output_file_name, size=size
|
||||
)
|
||||
|
@ -131,7 +134,7 @@ def main(options) -> None:
|
|||
return True
|
||||
|
||||
constructor: Constructor = Constructor(
|
||||
map_,
|
||||
osm_data,
|
||||
flinger,
|
||||
scheme,
|
||||
icon_extractor,
|
||||
|
@ -141,17 +144,14 @@ def main(options) -> None:
|
|||
)
|
||||
constructor.construct()
|
||||
|
||||
painter: Painter = Painter(
|
||||
painter: Map = Map(
|
||||
overlap=options.overlap,
|
||||
mode=options.mode,
|
||||
label_mode=options.label_mode,
|
||||
map_=map_,
|
||||
flinger=flinger,
|
||||
svg=svg,
|
||||
icon_extractor=icon_extractor,
|
||||
scheme=scheme,
|
||||
)
|
||||
|
||||
painter.draw(constructor)
|
||||
|
||||
print(f"Writing output SVG to {options.output_file_name}...")
|
||||
|
@ -164,16 +164,18 @@ def draw_element(options):
|
|||
Draw single node, line, or area.
|
||||
"""
|
||||
if options.node:
|
||||
target = "node"
|
||||
target: str = "node"
|
||||
tags_description = options.node
|
||||
else:
|
||||
# Not implemented yet.
|
||||
sys.exit(1)
|
||||
|
||||
tags = dict([x.split("=") for x in tags_description.split(",")])
|
||||
scheme: Scheme = Scheme(Path("scheme/default.yml"))
|
||||
tags: dict[str, str] = dict(
|
||||
[x.split("=") for x in tags_description.split(",")]
|
||||
)
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
extractor: ShapeExtractor = ShapeExtractor(
|
||||
Path("icons/icons.svg"), Path("icons/config.json")
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
processed: Set[str] = set()
|
||||
icon, priority = scheme.get_icon(extractor, tags, processed)
|
||||
|
@ -193,8 +195,8 @@ def draw_element(options):
|
|||
size: np.array = point.get_size() + border
|
||||
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
|
||||
|
||||
Path("out").mkdir(parents=True, exist_ok=True)
|
||||
svg = svgwrite.Drawing("out/element.svg", size.astype(float))
|
||||
output_file_path: Path = workspace.output_path / "element.svg"
|
||||
svg = svgwrite.Drawing(str(output_file_path), size.astype(float))
|
||||
for style in scheme.get_style(tags, 18):
|
||||
style: LineStyle
|
||||
path = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
|
||||
|
@ -203,44 +205,47 @@ def draw_element(options):
|
|||
point.draw_main_shapes(svg)
|
||||
point.draw_extra_shapes(svg)
|
||||
point.draw_texts(svg)
|
||||
svg.write(open("out/element.svg", "w+"))
|
||||
with output_file_path.open("w+") as output_file:
|
||||
svg.write(output_file)
|
||||
logging.info(f"Element is written to {output_file_path}.")
|
||||
|
||||
|
||||
def init_scheme() -> Scheme:
|
||||
return Scheme(Path("scheme/default.yml"))
|
||||
return Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
|
||||
workspace: Workspace = Workspace(Path("out"))
|
||||
|
||||
options: argparse.Namespace = parse_options(sys.argv)
|
||||
arguments: argparse.Namespace = parse_options(sys.argv)
|
||||
|
||||
if options.command == "render":
|
||||
main(options)
|
||||
if arguments.command == "render":
|
||||
main(arguments)
|
||||
|
||||
elif options.command == "tile":
|
||||
elif arguments.command == "tile":
|
||||
from roentgen import tile
|
||||
|
||||
tile.ui(options)
|
||||
tile.ui(arguments)
|
||||
|
||||
elif options.command == "icons":
|
||||
elif arguments.command == "icons":
|
||||
draw_icons()
|
||||
|
||||
elif options.command == "mapcss":
|
||||
elif arguments.command == "mapcss":
|
||||
from roentgen import mapcss
|
||||
|
||||
mapcss.ui(options)
|
||||
mapcss.ui(arguments)
|
||||
|
||||
elif options.command == "element":
|
||||
draw_element(options)
|
||||
elif arguments.command == "element":
|
||||
draw_element(arguments)
|
||||
|
||||
elif options.command == "server":
|
||||
elif arguments.command == "server":
|
||||
from roentgen import server
|
||||
|
||||
server.ui(options)
|
||||
server.ui(arguments)
|
||||
|
||||
elif options.command == "taginfo":
|
||||
elif arguments.command == "taginfo":
|
||||
from roentgen.taginfo import write_taginfo_project_file
|
||||
|
||||
write_taginfo_project_file(init_scheme())
|
||||
|
|
|
@ -37,6 +37,7 @@ def get_gradient_color(
|
|||
range_coefficient: float = (
|
||||
0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta()
|
||||
)
|
||||
# If value is out of range, set it to boundary value.
|
||||
range_coefficient = min(1.0, max(0.0, range_coefficient))
|
||||
index: int = int(range_coefficient * color_length)
|
||||
coefficient: float = (
|
||||
|
|
|
@ -4,31 +4,33 @@ Construct Röntgen nodes and ways.
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set
|
||||
from typing import Any, Iterator, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from colour import Color
|
||||
|
||||
from roentgen import ui
|
||||
from roentgen.color import get_gradient_color
|
||||
from roentgen.figure import Building, Road, StyledFigure
|
||||
from roentgen.figure import Building, Road, StyledFigure, Tree, DirectionSector
|
||||
from roentgen.flinger import Flinger
|
||||
|
||||
# fmt: off
|
||||
from roentgen.icon import (
|
||||
DEFAULT_SMALL_SHAPE_ID, Icon, IconSet, ShapeExtractor, ShapeSpecification
|
||||
)
|
||||
from roentgen.osm_reader import Map, OSMNode, OSMRelation, OSMWay, Tagged
|
||||
from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay
|
||||
from roentgen.point import Point
|
||||
from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme
|
||||
from roentgen.ui import TIME_MODE, AUTHOR_MODE
|
||||
from roentgen.util import MinMax
|
||||
|
||||
# fmt: on
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
DEBUG: bool = False
|
||||
TIME_COLOR_SCALE: List[Color] = [
|
||||
TIME_COLOR_SCALE: list[Color] = [
|
||||
Color("#581845"),
|
||||
Color("#900C3F"),
|
||||
Color("#C70039"),
|
||||
|
@ -38,14 +40,14 @@ TIME_COLOR_SCALE: List[Color] = [
|
|||
]
|
||||
|
||||
|
||||
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
|
||||
def line_center(nodes: list[OSMNode], flinger: Flinger) -> np.array:
|
||||
"""
|
||||
Get geometric center of nodes set.
|
||||
|
||||
:param nodes: node list
|
||||
:param flinger: flinger that remap geo positions
|
||||
"""
|
||||
boundary: List[MinMax] = [MinMax(), MinMax()]
|
||||
boundary: list[MinMax] = [MinMax(), MinMax()]
|
||||
|
||||
for node in nodes:
|
||||
boundary[0].update(node.coordinates[0])
|
||||
|
@ -74,14 +76,14 @@ def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color:
|
|||
return get_gradient_color(time, boundaries, TIME_COLOR_SCALE)
|
||||
|
||||
|
||||
def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
|
||||
def glue(ways: list[OSMWay]) -> list[list[OSMNode]]:
|
||||
"""
|
||||
Try to glue ways that share nodes.
|
||||
|
||||
:param ways: ways to glue
|
||||
"""
|
||||
result: List[List[OSMNode]] = []
|
||||
to_process: Set[OSMWay] = set()
|
||||
result: list[list[OSMNode]] = []
|
||||
to_process: set[OSMWay] = set()
|
||||
|
||||
for way in ways:
|
||||
if way.is_cycle():
|
||||
|
@ -112,9 +114,7 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
|
|||
|
||||
|
||||
def is_cycle(nodes) -> bool:
|
||||
"""
|
||||
Is way a cycle way or an area boundary.
|
||||
"""
|
||||
"""Is way a cycle way or an area boundary."""
|
||||
return nodes[0] == nodes[-1]
|
||||
|
||||
|
||||
|
@ -125,7 +125,7 @@ class Constructor:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
map_: Map,
|
||||
osm_data: OSMData,
|
||||
flinger: Flinger,
|
||||
scheme: Scheme,
|
||||
icon_extractor: ShapeExtractor,
|
||||
|
@ -136,59 +136,51 @@ class Constructor:
|
|||
self.check_level = check_level
|
||||
self.mode: str = mode
|
||||
self.seed: str = seed
|
||||
self.map_: Map = map_
|
||||
self.osm_data: OSMData = osm_data
|
||||
self.flinger: Flinger = flinger
|
||||
self.scheme: Scheme = scheme
|
||||
self.icon_extractor = icon_extractor
|
||||
|
||||
self.points: List[Point] = []
|
||||
self.figures: List[StyledFigure] = []
|
||||
self.buildings: List[Building] = []
|
||||
self.roads: List[Road] = []
|
||||
self.points: list[Point] = []
|
||||
self.figures: list[StyledFigure] = []
|
||||
self.buildings: list[Building] = []
|
||||
self.roads: list[Road] = []
|
||||
self.trees: list[Tree] = []
|
||||
self.direction_sectors: list[DirectionSector] = []
|
||||
|
||||
self.heights: Set[float] = {2, 4}
|
||||
self.heights: set[float] = {2, 4}
|
||||
|
||||
def add_building(self, building: Building) -> None:
|
||||
"""
|
||||
Add building and update levels.
|
||||
"""
|
||||
"""Add building and update levels."""
|
||||
self.buildings.append(building)
|
||||
self.heights.add(building.height)
|
||||
self.heights.add(building.min_height)
|
||||
|
||||
def construct(self) -> None:
|
||||
"""
|
||||
Construct nodes, ways, and relations.
|
||||
"""
|
||||
"""Construct nodes, ways, and relations."""
|
||||
self.construct_ways()
|
||||
self.construct_relations()
|
||||
self.construct_nodes()
|
||||
|
||||
def construct_ways(self) -> None:
|
||||
"""
|
||||
Construct Röntgen ways.
|
||||
"""
|
||||
way_number: int = 0
|
||||
for way_id in self.map_.ways:
|
||||
"""Construct Röntgen ways."""
|
||||
for index, way_id in enumerate(self.osm_data.ways):
|
||||
ui.progress_bar(
|
||||
way_number,
|
||||
len(self.map_.ways),
|
||||
index,
|
||||
len(self.osm_data.ways),
|
||||
step=10,
|
||||
text="Constructing ways",
|
||||
)
|
||||
way_number += 1
|
||||
way: OSMWay = self.map_.ways[way_id]
|
||||
if not self.check_level(way.tags):
|
||||
continue
|
||||
way: OSMWay = self.osm_data.ways[way_id]
|
||||
self.construct_line(way, [], [way.nodes])
|
||||
|
||||
ui.progress_bar(-1, len(self.map_.ways), text="Constructing ways")
|
||||
ui.progress_bar(-1, len(self.osm_data.ways), text="Constructing ways")
|
||||
|
||||
def construct_line(
|
||||
self,
|
||||
line: Optional[Tagged],
|
||||
inners: List[List[OSMNode]],
|
||||
outers: List[List[OSMNode]],
|
||||
line: Union[OSMWay, OSMRelation],
|
||||
inners: list[list[OSMNode]],
|
||||
outers: list[list[OSMNode]],
|
||||
) -> None:
|
||||
"""
|
||||
Way or relation construction.
|
||||
|
@ -199,22 +191,22 @@ class Constructor:
|
|||
"""
|
||||
assert len(outers) >= 1
|
||||
|
||||
if not self.check_level(line.tags):
|
||||
return
|
||||
|
||||
center_point, center_coordinates = line_center(outers[0], self.flinger)
|
||||
if self.mode in ["user-coloring", "time"]:
|
||||
if self.mode == "user-coloring":
|
||||
if self.mode in [AUTHOR_MODE, TIME_MODE]:
|
||||
color: Color
|
||||
if self.mode == AUTHOR_MODE:
|
||||
color = get_user_color(line.user, self.seed)
|
||||
else: # self.mode == "time":
|
||||
color = get_time_color(line.timestamp, self.map_.time)
|
||||
else: # self.mode == TIME_MODE
|
||||
color = get_time_color(line.timestamp, self.osm_data.time)
|
||||
self.draw_special_mode(inners, line, outers, color)
|
||||
return
|
||||
|
||||
if not line.tags:
|
||||
return
|
||||
|
||||
scale: float = self.flinger.get_scale(center_coordinates)
|
||||
|
||||
line_styles: List[LineStyle] = self.scheme.get_style(line.tags, scale)
|
||||
|
||||
if "building:part" in line.tags or "building" in line.tags:
|
||||
self.add_building(
|
||||
Building(line.tags, inners, outers, self.flinger, self.scheme)
|
||||
|
@ -225,6 +217,9 @@ class Constructor:
|
|||
self.roads.append(Road(line.tags, inners, outers, road_matcher))
|
||||
return
|
||||
|
||||
scale: float = self.flinger.get_scale(center_coordinates)
|
||||
line_styles: list[LineStyle] = self.scheme.get_style(line.tags, scale)
|
||||
|
||||
for line_style in line_styles:
|
||||
self.figures.append(
|
||||
StyledFigure(line.tags, inners, outers, line_style)
|
||||
|
@ -235,7 +230,7 @@ class Constructor:
|
|||
and line.get_tag("area") != "no"
|
||||
and self.scheme.is_area(line.tags)
|
||||
):
|
||||
processed: Set[str] = set()
|
||||
processed: set[str] = set()
|
||||
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
|
@ -257,7 +252,7 @@ class Constructor:
|
|||
|
||||
if not line_styles:
|
||||
if DEBUG:
|
||||
style: Dict[str, Any] = {
|
||||
style: dict[str, Any] = {
|
||||
"fill": "none",
|
||||
"stroke": Color("red").hex,
|
||||
"stroke-width": 1,
|
||||
|
@ -267,7 +262,7 @@ class Constructor:
|
|||
)
|
||||
self.figures.append(figure)
|
||||
|
||||
processed: Set[str] = set()
|
||||
processed: set[str] = set()
|
||||
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
|
@ -285,7 +280,7 @@ class Constructor:
|
|||
"""
|
||||
Add figure for special mode: time or author.
|
||||
"""
|
||||
style: Dict[str, Any] = {
|
||||
style: dict[str, Any] = {
|
||||
"fill": "none",
|
||||
"stroke": color.hex,
|
||||
"stroke-width": 1,
|
||||
|
@ -298,84 +293,94 @@ class Constructor:
|
|||
"""
|
||||
Construct Röntgen ways from OSM relations.
|
||||
"""
|
||||
for relation_id in self.map_.relations:
|
||||
relation: OSMRelation = self.map_.relations[relation_id]
|
||||
for relation_id in self.osm_data.relations:
|
||||
relation: OSMRelation = self.osm_data.relations[relation_id]
|
||||
tags = relation.tags
|
||||
if not self.check_level(tags):
|
||||
continue
|
||||
if "type" not in tags or tags["type"] != "multipolygon":
|
||||
continue
|
||||
inner_ways: List[OSMWay] = []
|
||||
outer_ways: List[OSMWay] = []
|
||||
inner_ways: list[OSMWay] = []
|
||||
outer_ways: list[OSMWay] = []
|
||||
for member in relation.members:
|
||||
if member.type_ == "way":
|
||||
if member.role == "inner":
|
||||
if member.ref in self.map_.ways:
|
||||
inner_ways.append(self.map_.ways[member.ref])
|
||||
if member.ref in self.osm_data.ways:
|
||||
inner_ways.append(self.osm_data.ways[member.ref])
|
||||
elif member.role == "outer":
|
||||
if member.ref in self.map_.ways:
|
||||
outer_ways.append(self.map_.ways[member.ref])
|
||||
if member.ref in self.osm_data.ways:
|
||||
outer_ways.append(self.osm_data.ways[member.ref])
|
||||
else:
|
||||
logging.warning(f'Unknown member role "{member.role}".')
|
||||
if outer_ways:
|
||||
inners_path: List[List[OSMNode]] = glue(inner_ways)
|
||||
outers_path: List[List[OSMNode]] = glue(outer_ways)
|
||||
inners_path: list[list[OSMNode]] = glue(inner_ways)
|
||||
outers_path: list[list[OSMNode]] = glue(outer_ways)
|
||||
self.construct_line(relation, inners_path, outers_path)
|
||||
|
||||
def construct_nodes(self) -> None:
|
||||
"""
|
||||
Draw nodes.
|
||||
"""
|
||||
node_number: int = 0
|
||||
|
||||
sorted_node_ids: Iterator[int] = sorted(
|
||||
self.map_.nodes.keys(),
|
||||
key=lambda x: -self.map_.nodes[x].coordinates[0],
|
||||
self.osm_data.nodes.keys(),
|
||||
key=lambda x: -self.osm_data.nodes[x].coordinates[0],
|
||||
)
|
||||
|
||||
for node_id in sorted_node_ids:
|
||||
processed: Set[str] = set()
|
||||
|
||||
node_number += 1
|
||||
for index, node_id in enumerate(sorted_node_ids):
|
||||
ui.progress_bar(
|
||||
node_number, len(self.map_.nodes), text="Constructing nodes"
|
||||
index, len(self.osm_data.nodes), text="Constructing nodes"
|
||||
)
|
||||
node: OSMNode = self.map_.nodes[node_id]
|
||||
flung = self.flinger.fling(node.coordinates)
|
||||
tags = node.tags
|
||||
self.construct_node(self.osm_data.nodes[node_id])
|
||||
ui.progress_bar(-1, len(self.osm_data.nodes), text="Constructing nodes")
|
||||
|
||||
if not self.check_level(tags):
|
||||
continue
|
||||
def construct_node(self, node: OSMNode) -> None:
|
||||
tags = node.tags
|
||||
if not self.check_level(tags):
|
||||
return
|
||||
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
draw_outline: bool = True
|
||||
processed: set[str] = set()
|
||||
|
||||
if self.mode in ["time", "user-coloring"]:
|
||||
if not tags:
|
||||
continue
|
||||
color = DEFAULT_COLOR
|
||||
if self.mode == "user-coloring":
|
||||
color = get_user_color(node.user, self.seed)
|
||||
if self.mode == "time":
|
||||
color = get_time_color(node.timestamp, self.map_.time)
|
||||
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
|
||||
icon_set = IconSet(
|
||||
Icon([ShapeSpecification(dot, color)]), [], set()
|
||||
)
|
||||
priority = 0
|
||||
draw_outline = False
|
||||
labels = []
|
||||
else:
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.icon_extractor, tags, processed
|
||||
)
|
||||
labels = self.scheme.construct_text(tags, "all", processed)
|
||||
self.scheme.process_ignored(tags, processed)
|
||||
flung = self.flinger.fling(node.coordinates)
|
||||
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
draw_outline: bool = True
|
||||
|
||||
if self.mode in [TIME_MODE, AUTHOR_MODE]:
|
||||
if not tags:
|
||||
return
|
||||
color: Color = DEFAULT_COLOR
|
||||
if self.mode == AUTHOR_MODE:
|
||||
color = get_user_color(node.user, self.seed)
|
||||
if self.mode == TIME_MODE:
|
||||
color = get_time_color(node.timestamp, self.osm_data.time)
|
||||
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
|
||||
icon_set = IconSet(
|
||||
Icon([ShapeSpecification(dot, color)]), [], set()
|
||||
)
|
||||
point: Point = Point(
|
||||
icon_set, labels, tags, processed, flung, node.coordinates,
|
||||
priority=priority, draw_outline=draw_outline
|
||||
icon_set, [], tags, processed, flung, node.coordinates,
|
||||
priority=0, draw_outline=False
|
||||
) # fmt: skip
|
||||
self.points.append(point)
|
||||
return
|
||||
|
||||
ui.progress_bar(-1, len(self.map_.nodes), text="Constructing nodes")
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.icon_extractor, tags, processed
|
||||
)
|
||||
labels = self.scheme.construct_text(tags, "all", processed)
|
||||
self.scheme.process_ignored(tags, processed)
|
||||
|
||||
if node.get_tag("natural") == "tree" and (
|
||||
"diameter_crown" in node.tags or "circumference" in node.tags
|
||||
):
|
||||
self.trees.append(Tree(tags, node.coordinates, flung))
|
||||
return
|
||||
|
||||
if "direction" in node.tags or "camera:direction" in node.tags:
|
||||
self.direction_sectors.append(DirectionSector(tags, flung))
|
||||
point: Point = Point(
|
||||
icon_set, labels, tags, processed, flung, node.coordinates,
|
||||
priority=priority, draw_outline=draw_outline
|
||||
) # fmt: skip
|
||||
self.points.append(point)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Direction tag support.
|
||||
"""
|
||||
from typing import Iterator, List, Optional, Union
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from portolan import middle
|
||||
|
@ -72,7 +72,7 @@ class Sector:
|
|||
self.main_direction: Optional[np.array] = None
|
||||
|
||||
if "-" in text:
|
||||
parts: List[str] = text.split("-")
|
||||
parts: list[str] = text.split("-")
|
||||
self.start = parse_vector(parts[0])
|
||||
self.end = parse_vector(parts[1])
|
||||
self.main_direction = (self.start + self.end) / 2
|
||||
|
@ -90,7 +90,7 @@ class Sector:
|
|||
self.start = np.dot(rotation_matrix(result_angle), vector)
|
||||
self.end = np.dot(rotation_matrix(-result_angle), vector)
|
||||
|
||||
def draw(self, center: np.array, radius: float) -> Optional[List[SVGPath]]:
|
||||
def draw(self, center: np.array, radius: float) -> Optional[list[SVGPath]]:
|
||||
"""
|
||||
Construct SVG path commands for arc element.
|
||||
|
||||
|
@ -139,7 +139,7 @@ class DirectionSet:
|
|||
def __str__(self) -> str:
|
||||
return ", ".join(map(str, self.sectors))
|
||||
|
||||
def draw(self, center: np.array, radius: float) -> Iterator[List[SVGPath]]:
|
||||
def draw(self, center: np.array, radius: float) -> Iterator[list[SVGPath]]:
|
||||
"""
|
||||
Construct SVG "d" for arc elements.
|
||||
|
||||
|
@ -159,7 +159,7 @@ class DirectionSet:
|
|||
:return: true if direction is right, false if direction is left, and
|
||||
None otherwise.
|
||||
"""
|
||||
result: List[bool] = [x.is_right() for x in self.sectors]
|
||||
result: list[bool] = [x.is_right() for x in self.sectors]
|
||||
if result == [True] * len(result):
|
||||
return True
|
||||
if result == [False] * len(result):
|
||||
|
|
|
@ -9,10 +9,13 @@ import cairo
|
|||
import numpy as np
|
||||
import svgwrite
|
||||
from colour import Color
|
||||
from svgwrite.shapes import Rect
|
||||
from svgwrite.path import Path as SVGPath
|
||||
from svgwrite.shapes import Rect
|
||||
from svgwrite.text import Text
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Style:
|
||||
|
|
|
@ -4,7 +4,11 @@ Figures displayed on the map.
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
from colour import Color
|
||||
from svgwrite import Drawing
|
||||
from svgwrite.path import Path
|
||||
|
||||
from roentgen.direction import Sector, DirectionSet
|
||||
from roentgen.flinger import Flinger
|
||||
from roentgen.osm_reader import OSMNode, Tagged
|
||||
from roentgen.road import Lane
|
||||
|
@ -97,14 +101,85 @@ class Building(Figure):
|
|||
if levels:
|
||||
self.min_height = float(levels) * 2.5
|
||||
|
||||
height: Optional[str] = self.get_length("height")
|
||||
height: Optional[float] = self.get_length("height")
|
||||
if height:
|
||||
self.height = height
|
||||
|
||||
height: Optional[str] = self.get_length("min_height")
|
||||
height: Optional[float] = self.get_length("min_height")
|
||||
if height:
|
||||
self.min_height = height
|
||||
|
||||
def draw_shade(self, building_shade, flinger: Flinger) -> None:
|
||||
"""Draw shade casted by the building."""
|
||||
scale: float = flinger.get_scale() / 3.0
|
||||
shift_1 = np.array((scale * self.min_height, 0))
|
||||
shift_2 = np.array((scale * self.height, 0))
|
||||
commands: str = self.get_path(flinger, shift_1)
|
||||
path = Path(
|
||||
d=commands, fill="#000000", stroke="#000000", stroke_width=1
|
||||
)
|
||||
building_shade.add(path)
|
||||
for nodes in self.inners + self.outers:
|
||||
for i in range(len(nodes) - 1):
|
||||
flung_1 = flinger.fling(nodes[i].coordinates)
|
||||
flung_2 = flinger.fling(nodes[i + 1].coordinates)
|
||||
command = (
|
||||
"M",
|
||||
np.add(flung_1, shift_1),
|
||||
"L",
|
||||
np.add(flung_2, shift_1),
|
||||
np.add(flung_2, shift_2),
|
||||
np.add(flung_1, shift_2),
|
||||
"Z",
|
||||
)
|
||||
path = Path(
|
||||
command, fill="#000000", stroke="#000000", stroke_width=1
|
||||
)
|
||||
building_shade.add(path)
|
||||
|
||||
def draw_walls(
|
||||
self, svg: Drawing, height: float, previous_height: float, scale: float
|
||||
) -> None:
|
||||
"""Draw building walls."""
|
||||
shift_1 = [0, -previous_height * scale]
|
||||
shift_2 = [0, -height * scale]
|
||||
for segment in self.parts:
|
||||
if height == 2:
|
||||
fill = Color("#AAAAAA")
|
||||
elif height == 4:
|
||||
fill = Color("#C3C3C3")
|
||||
else:
|
||||
color_part: float = 0.8 + segment.angle * 0.2
|
||||
fill = Color(rgb=(color_part, color_part, color_part))
|
||||
|
||||
command = (
|
||||
"M",
|
||||
segment.point_1 + shift_1,
|
||||
"L",
|
||||
segment.point_2 + shift_1,
|
||||
segment.point_2 + shift_2,
|
||||
segment.point_1 + shift_2,
|
||||
segment.point_1 + shift_1,
|
||||
"Z",
|
||||
)
|
||||
path = svg.path(
|
||||
d=command,
|
||||
fill=fill.hex,
|
||||
stroke=fill.hex,
|
||||
stroke_width=1,
|
||||
stroke_linejoin="round",
|
||||
)
|
||||
svg.add(path)
|
||||
|
||||
def draw_roof(self, svg: Drawing, flinger: Flinger, scale: float):
|
||||
"""Draw building roof."""
|
||||
path: Path = Path(
|
||||
d=self.get_path(flinger, np.array([0, -self.height * scale]))
|
||||
)
|
||||
path.update(self.line_style.style)
|
||||
path.update({"stroke-linejoin": "round"})
|
||||
svg.add(path)
|
||||
|
||||
|
||||
class StyledFigure(Figure):
|
||||
"""
|
||||
|
@ -161,14 +236,110 @@ class Road(Figure):
|
|||
pass
|
||||
|
||||
|
||||
class Tree(Tagged):
|
||||
"""
|
||||
Tree on the map.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, tags: dict[str, str], coordinates: np.array, point: np.array
|
||||
):
|
||||
super().__init__(tags)
|
||||
self.coordinates: np.array = coordinates
|
||||
self.point: np.array = point
|
||||
|
||||
def draw(self, svg: Drawing, flinger: Flinger, scheme: Scheme):
|
||||
"""Draw crown and trunk."""
|
||||
scale: float = flinger.get_scale(self.coordinates)
|
||||
radius: float
|
||||
if "diameter_crown" in self.tags:
|
||||
radius = float(self.tags["diameter_crown"]) / 2.0
|
||||
else:
|
||||
radius = 2.0
|
||||
color: Color = scheme.get_color("evergreen_color")
|
||||
svg.add(svg.circle(self.point, radius * scale, fill=color, opacity=0.3))
|
||||
|
||||
if "circumference" in self.tags:
|
||||
radius: float = float(self.tags["circumference"]) / 2.0 / np.pi
|
||||
svg.add(svg.circle(self.point, radius * scale, fill="#B89A74"))
|
||||
|
||||
|
||||
class DirectionSector(Tagged):
|
||||
"""
|
||||
Sector that represents direction.
|
||||
"""
|
||||
|
||||
def __init__(self, tags: dict[str, str], point):
|
||||
super().__init__(tags)
|
||||
self.point = point
|
||||
|
||||
def draw(self, svg: Drawing, scheme: Scheme):
|
||||
"""Draw gradient sector."""
|
||||
angle = None
|
||||
is_revert_gradient: bool = False
|
||||
|
||||
if self.get_tag("man_made") == "surveillance":
|
||||
direction = self.get_tag("camera:direction")
|
||||
if "camera:angle" in self.tags:
|
||||
angle = float(self.get_tag("camera:angle"))
|
||||
if "angle" in self.tags:
|
||||
angle = float(self.get_tag("angle"))
|
||||
direction_radius: float = 25
|
||||
direction_color: Color = scheme.get_color("direction_camera_color")
|
||||
elif self.get_tag("traffic_sign") == "stop":
|
||||
direction = self.get_tag("direction")
|
||||
direction_radius: float = 25
|
||||
direction_color: Color = Color("red")
|
||||
else:
|
||||
direction = self.get_tag("direction")
|
||||
direction_radius: float = 50
|
||||
direction_color: Color = scheme.get_color("direction_view_color")
|
||||
is_revert_gradient = True
|
||||
|
||||
if not direction:
|
||||
return
|
||||
|
||||
point = (self.point.astype(int)).astype(float)
|
||||
|
||||
if angle:
|
||||
paths = [Sector(direction, angle).draw(point, direction_radius)]
|
||||
else:
|
||||
paths = DirectionSet(direction).draw(point, direction_radius)
|
||||
|
||||
for path in paths:
|
||||
radial_gradient = svg.radialGradient(
|
||||
center=point,
|
||||
r=direction_radius,
|
||||
gradientUnits="userSpaceOnUse",
|
||||
)
|
||||
gradient = svg.defs.add(radial_gradient)
|
||||
if is_revert_gradient:
|
||||
(
|
||||
gradient
|
||||
.add_stop_color(0, direction_color.hex, opacity=0)
|
||||
.add_stop_color(1, direction_color.hex, opacity=0.7)
|
||||
) # fmt: skip
|
||||
else:
|
||||
(
|
||||
gradient
|
||||
.add_stop_color(0, direction_color.hex, opacity=0.4)
|
||||
.add_stop_color(1, direction_color.hex, opacity=0)
|
||||
) # fmt: skip
|
||||
path_element: Path = svg.path(
|
||||
d=["M", point] + path + ["L", point, "Z"],
|
||||
fill=gradient.get_paint_server(),
|
||||
)
|
||||
svg.add(path_element)
|
||||
|
||||
|
||||
class Segment:
|
||||
"""
|
||||
Line segment.
|
||||
"""
|
||||
|
||||
def __init__(self, point_1: np.array, point_2: np.array):
|
||||
self.point_1 = point_1
|
||||
self.point_2 = point_2
|
||||
self.point_1: np.array = point_1
|
||||
self.point_2: np.array = point_2
|
||||
|
||||
difference: np.array = point_2 - point_1
|
||||
vector: np.array = difference / np.linalg.norm(difference)
|
||||
|
@ -215,9 +386,7 @@ def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
|
|||
|
||||
|
||||
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
|
||||
"""
|
||||
Construct SVG path commands from nodes.
|
||||
"""
|
||||
"""Construct SVG path commands from nodes."""
|
||||
path: str = ""
|
||||
prev_node: Optional[OSMNode] = None
|
||||
for node in nodes:
|
||||
|
|
|
@ -27,10 +27,13 @@ def pseudo_mercator(coordinates: np.array) -> np.array:
|
|||
return np.array((coordinates[1], y))
|
||||
|
||||
|
||||
def osm_zoom_level_to_pixels_per_meter(zoom_level: float):
|
||||
def osm_zoom_level_to_pixels_per_meter(zoom_level: float) -> float:
|
||||
"""
|
||||
Convert OSM zoom level (see https://wiki.openstreetmap.org/wiki/Zoom_levels)
|
||||
to pixels per meter on Equator.
|
||||
Convert OSM zoom level to pixels per meter on Equator. See
|
||||
https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||
|
||||
:param zoom_level: integer number usually not bigger than 20, but this
|
||||
function allows any non-negative float value
|
||||
"""
|
||||
return 2 ** zoom_level / 156415
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ Icon grid drawing.
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from colour import Color
|
||||
|
@ -24,7 +24,7 @@ class IconCollection:
|
|||
Collection of icons.
|
||||
"""
|
||||
|
||||
icons: List[Icon]
|
||||
icons: list[Icon]
|
||||
|
||||
@classmethod
|
||||
def from_scheme(
|
||||
|
@ -45,7 +45,7 @@ class IconCollection:
|
|||
:param add_unused: create icons from shapes that have no corresponding
|
||||
tags
|
||||
"""
|
||||
icons: List[Icon] = []
|
||||
icons: list[Icon] = []
|
||||
|
||||
def add() -> Icon:
|
||||
"""
|
||||
|
@ -80,7 +80,7 @@ class IconCollection:
|
|||
continue
|
||||
for icon_id in matcher.under_icon:
|
||||
for icon_2_id in matcher.with_icon:
|
||||
current_set: List[str] = (
|
||||
current_set: list[str] = (
|
||||
[icon_id] + [icon_2_id] + matcher.over_icon
|
||||
)
|
||||
add()
|
||||
|
@ -99,7 +99,7 @@ class IconCollection:
|
|||
):
|
||||
add()
|
||||
|
||||
specified_ids: Set[str] = set()
|
||||
specified_ids: set[str] = set()
|
||||
|
||||
for icon in icons:
|
||||
specified_ids |= set(icon.get_shape_ids())
|
||||
|
@ -199,17 +199,18 @@ def draw_icons() -> None:
|
|||
Draw all possible icon shapes combinations as grid in one SVG file and as
|
||||
individual SVG files.
|
||||
"""
|
||||
icons_by_id_path: Path = workspace.get_icons_by_id_path()
|
||||
icons_by_name_path: Path = workspace.get_icons_by_name_path()
|
||||
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
collection: IconCollection = IconCollection.from_scheme(scheme, extractor)
|
||||
|
||||
icon_grid_path: Path = workspace.get_icon_grid_path()
|
||||
collection.draw_grid(icon_grid_path)
|
||||
logging.info(f"Icon grid is written to {icon_grid_path}.")
|
||||
|
||||
icons_by_id_path: Path = workspace.get_icons_by_id_path()
|
||||
icons_by_name_path: Path = workspace.get_icons_by_name_path()
|
||||
collection.draw_icons(icons_by_id_path)
|
||||
collection.draw_icons(icons_by_name_path, by_name=True)
|
||||
logging.info(
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any, Optional
|
||||
from xml.dom.minidom import Document, Element, parse
|
||||
|
||||
import numpy as np
|
||||
|
@ -44,13 +44,13 @@ class Shape:
|
|||
id_: str # shape identifier
|
||||
name: Optional[str] = None # icon description
|
||||
is_right_directed: Optional[bool] = None
|
||||
emojis: Set[str] = field(default_factory=set)
|
||||
emojis: set[str] = field(default_factory=set)
|
||||
is_part: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_structure(
|
||||
cls,
|
||||
structure: Dict[str, Any],
|
||||
structure: dict[str, Any],
|
||||
path: str,
|
||||
offset: np.array,
|
||||
id_: str,
|
||||
|
@ -102,7 +102,7 @@ class Shape:
|
|||
:param offset: additional offset
|
||||
:param scale: scale resulting image
|
||||
"""
|
||||
transformations: List[str] = []
|
||||
transformations: list[str] = []
|
||||
shift: np.array = point + offset
|
||||
|
||||
transformations.append(f"translate({shift[0]},{shift[1]})")
|
||||
|
@ -137,7 +137,7 @@ def verify_sketch_element(element, id_: str) -> bool:
|
|||
if not element.getAttribute("style"):
|
||||
return True
|
||||
|
||||
style: Dict = dict(
|
||||
style: dict = dict(
|
||||
[x.split(":") for x in element.getAttribute("style").split(";")]
|
||||
)
|
||||
if (
|
||||
|
@ -177,8 +177,8 @@ class ShapeExtractor:
|
|||
:param svg_file_name: input SVG file name with icons. File may contain
|
||||
any other irrelevant graphics.
|
||||
"""
|
||||
self.shapes: Dict[str, Shape] = {}
|
||||
self.configuration: Dict[str, Any] = json.load(
|
||||
self.shapes: dict[str, Shape] = {}
|
||||
self.configuration: dict[str, Any] = json.load(
|
||||
configuration_file_name.open()
|
||||
)
|
||||
with svg_file_name.open() as input_file:
|
||||
|
@ -233,7 +233,7 @@ class ShapeExtractor:
|
|||
name = child_node.childNodes[0].nodeValue
|
||||
break
|
||||
|
||||
configuration: Dict[str, Any] = (
|
||||
configuration: dict[str, Any] = (
|
||||
self.configuration[id_] if id_ in self.configuration else {}
|
||||
)
|
||||
self.shapes[id_] = Shape.from_structure(
|
||||
|
@ -326,7 +326,7 @@ class ShapeSpecification:
|
|||
self,
|
||||
svg,
|
||||
point: np.array,
|
||||
tags: Dict[str, Any] = None,
|
||||
tags: dict[str, Any] = None,
|
||||
outline: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -351,7 +351,7 @@ class ShapeSpecification:
|
|||
bright: bool = is_bright(self.color)
|
||||
color: Color = Color("black") if bright else Color("white")
|
||||
|
||||
style: Dict[str, Any] = {
|
||||
style: dict[str, Any] = {
|
||||
"fill": color.hex,
|
||||
"stroke": color.hex,
|
||||
"stroke-width": 2.2,
|
||||
|
@ -381,15 +381,15 @@ class Icon:
|
|||
Icon that consists of (probably) multiple shapes.
|
||||
"""
|
||||
|
||||
shape_specifications: List[ShapeSpecification]
|
||||
shape_specifications: list[ShapeSpecification]
|
||||
|
||||
def get_shape_ids(self) -> List[str]:
|
||||
def get_shape_ids(self) -> list[str]:
|
||||
"""
|
||||
Get all shape identifiers in the icon.
|
||||
"""
|
||||
return [x.shape.id_ for x in self.shape_specifications]
|
||||
|
||||
def get_names(self) -> List[str]:
|
||||
def get_names(self) -> list[str]:
|
||||
"""
|
||||
Gat all shape names in the icon.
|
||||
"""
|
||||
|
@ -402,7 +402,7 @@ class Icon:
|
|||
self,
|
||||
svg: svgwrite.Drawing,
|
||||
point: np.array,
|
||||
tags: Dict[str, Any] = None,
|
||||
tags: dict[str, Any] = None,
|
||||
outline: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
|
@ -469,7 +469,7 @@ class Icon:
|
|||
shape_specification.color = color
|
||||
|
||||
def add_specifications(
|
||||
self, specifications: List[ShapeSpecification]
|
||||
self, specifications: list[ShapeSpecification]
|
||||
) -> None:
|
||||
"""
|
||||
Add shape specifications to the icon.
|
||||
|
@ -494,8 +494,8 @@ class IconSet:
|
|||
"""
|
||||
|
||||
main_icon: Icon
|
||||
extra_icons: List[Icon]
|
||||
extra_icons: list[Icon]
|
||||
|
||||
# Tag keys that were processed to create icon set (other tag keys should be
|
||||
# displayed by text or ignored)
|
||||
processed: Set[str]
|
||||
processed: set[str]
|
||||
|
|
|
@ -3,7 +3,7 @@ MapCSS scheme creation.
|
|||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, TextIO
|
||||
from typing import Optional, TextIO
|
||||
|
||||
from colour import Color
|
||||
|
||||
|
@ -83,8 +83,8 @@ class MapCSSWriter:
|
|||
self.add_icons_for_lifecycle: bool = add_icons_for_lifecycle
|
||||
self.icon_directory_name: str = icon_directory_name
|
||||
|
||||
self.point_matchers: List[Matcher] = scheme.node_matchers
|
||||
self.line_matchers: List[Matcher] = scheme.way_matchers
|
||||
self.point_matchers: list[Matcher] = scheme.node_matchers
|
||||
self.line_matchers: list[Matcher] = scheme.way_matchers
|
||||
|
||||
def add_selector(
|
||||
self,
|
||||
|
@ -102,7 +102,7 @@ class MapCSSWriter:
|
|||
:param opacity: icon opacity
|
||||
:return:
|
||||
"""
|
||||
elements: Dict[str, str] = {}
|
||||
elements: dict[str, str] = {}
|
||||
|
||||
clean_shapes = matcher.get_clean_shapes()
|
||||
if clean_shapes:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Simple OpenStreetMap renderer.
|
||||
"""
|
||||
from typing import Any, Dict, Iterator, Set
|
||||
from typing import Any, Iterator
|
||||
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
|
@ -12,33 +12,27 @@ from svgwrite.shapes import Rect
|
|||
|
||||
from roentgen import ui
|
||||
from roentgen.constructor import Constructor
|
||||
from roentgen.direction import DirectionSet, Sector
|
||||
from roentgen.figure import Road
|
||||
from roentgen.flinger import Flinger
|
||||
from roentgen.icon import ShapeExtractor
|
||||
from roentgen.osm_reader import Map, OSMNode
|
||||
from roentgen.osm_reader import OSMNode
|
||||
from roentgen.point import Occupied
|
||||
from roentgen.road import Intersection, RoadPart
|
||||
from roentgen.scheme import Scheme
|
||||
from roentgen.ui import AUTHOR_MODE, TIME_MODE
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
AUTHOR_MODE = "user-coloring"
|
||||
CREATION_TIME_MODE = "time"
|
||||
|
||||
|
||||
class Painter:
|
||||
class Map:
|
||||
"""
|
||||
Map drawing.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
map_: Map,
|
||||
flinger: Flinger,
|
||||
svg: svgwrite.Drawing,
|
||||
icon_extractor: ShapeExtractor,
|
||||
scheme: Scheme,
|
||||
overlap: int = 12,
|
||||
mode: str = "normal",
|
||||
|
@ -48,20 +42,16 @@ class Painter:
|
|||
self.mode: str = mode
|
||||
self.label_mode: str = label_mode
|
||||
|
||||
self.map_: Map = map_
|
||||
self.flinger: Flinger = flinger
|
||||
self.svg: svgwrite.Drawing = svg
|
||||
self.icon_extractor = icon_extractor
|
||||
self.scheme: Scheme = scheme
|
||||
|
||||
self.background_color: Color = self.scheme.get_color("background_color")
|
||||
if self.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
|
||||
if self.mode in [AUTHOR_MODE, TIME_MODE]:
|
||||
self.background_color: Color = Color("#111111")
|
||||
|
||||
def draw(self, constructor: Constructor) -> None:
|
||||
"""
|
||||
Draw map.
|
||||
"""
|
||||
"""Draw map."""
|
||||
self.svg.add(
|
||||
Rect((0, 0), self.flinger.size, fill=self.background_color)
|
||||
)
|
||||
|
@ -84,9 +74,13 @@ class Painter:
|
|||
for road in roads:
|
||||
self.draw_road(road, road.matcher.color)
|
||||
|
||||
self.draw_trees(constructor)
|
||||
for tree in constructor.trees:
|
||||
tree.draw(self.svg, self.flinger, self.scheme)
|
||||
|
||||
for direction_sector in constructor.direction_sectors:
|
||||
direction_sector.draw(self.svg, self.scheme)
|
||||
|
||||
self.draw_buildings(constructor)
|
||||
self.draw_direction(constructor)
|
||||
|
||||
# All other points
|
||||
|
||||
|
@ -101,10 +95,6 @@ class Painter:
|
|||
steps: int = len(nodes)
|
||||
|
||||
for index, node in enumerate(nodes):
|
||||
if node.get_tag("natural") == "tree" and (
|
||||
"diameter_crown" in node.tags or "circumference" in node.tags
|
||||
):
|
||||
continue
|
||||
ui.progress_bar(
|
||||
index, steps * 3, step=10, text="Drawing main icons"
|
||||
)
|
||||
|
@ -121,207 +111,39 @@ class Painter:
|
|||
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
|
||||
)
|
||||
if (
|
||||
self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]
|
||||
self.mode not in [TIME_MODE, AUTHOR_MODE]
|
||||
and self.label_mode != "no"
|
||||
):
|
||||
point.draw_texts(self.svg, occupied, self.label_mode)
|
||||
|
||||
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
|
||||
|
||||
def draw_trees(self, constructor) -> None:
|
||||
"""
|
||||
Draw trunk and circumference.
|
||||
"""
|
||||
for node in constructor.points:
|
||||
if not (
|
||||
node.get_tag("natural") == "tree"
|
||||
and (
|
||||
"diameter_crown" in node.tags
|
||||
or "circumference" in node.tags
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
scale: float = self.flinger.get_scale(node.coordinates)
|
||||
|
||||
if "circumference" in node.tags:
|
||||
if "diameter_crown" in node.tags:
|
||||
opacity = 0.7
|
||||
radius = float(node.tags["diameter_crown"]) / 2
|
||||
else:
|
||||
opacity = 0.3
|
||||
radius = 2
|
||||
self.svg.add(
|
||||
self.svg.circle(
|
||||
node.point,
|
||||
radius * scale,
|
||||
fill=self.scheme.get_color("evergreen_color"),
|
||||
opacity=opacity,
|
||||
)
|
||||
)
|
||||
radius = float(node.tags["circumference"]) / 2 / np.pi
|
||||
self.svg.add(
|
||||
self.svg.circle(node.point, radius * scale, fill="#B89A74")
|
||||
)
|
||||
|
||||
def draw_buildings(self, constructor: Constructor) -> None:
|
||||
"""
|
||||
Draw buildings: shade, walls, and roof.
|
||||
"""
|
||||
# Draw shade.
|
||||
|
||||
"""Draw buildings: shade, walls, and roof."""
|
||||
building_shade: Group = Group(opacity=0.1)
|
||||
scale: float = self.flinger.get_scale() / 3.0
|
||||
for building in constructor.buildings:
|
||||
shift_1 = np.array((scale * building.min_height, 0))
|
||||
shift_2 = np.array((scale * building.height, 0))
|
||||
commands: str = building.get_path(self.flinger, shift_1)
|
||||
path = Path(
|
||||
d=commands, fill="#000000", stroke="#000000", stroke_width=1
|
||||
)
|
||||
building_shade.add(path)
|
||||
for nodes in building.inners + building.outers:
|
||||
for i in range(len(nodes) - 1):
|
||||
flung_1 = self.flinger.fling(nodes[i].coordinates)
|
||||
flung_2 = self.flinger.fling(nodes[i + 1].coordinates)
|
||||
command = (
|
||||
"M",
|
||||
np.add(flung_1, shift_1),
|
||||
"L",
|
||||
np.add(flung_2, shift_1),
|
||||
np.add(flung_2, shift_2),
|
||||
np.add(flung_1, shift_2),
|
||||
"Z",
|
||||
)
|
||||
path = Path(
|
||||
command,
|
||||
fill="#000000",
|
||||
stroke="#000000",
|
||||
stroke_width=1,
|
||||
)
|
||||
building_shade.add(path)
|
||||
building.draw_shade(building_shade, self.flinger)
|
||||
self.svg.add(building_shade)
|
||||
|
||||
# Draw buildings.
|
||||
|
||||
previous_height: float = 0
|
||||
count: int = len(constructor.heights)
|
||||
for index, height in enumerate(sorted(constructor.heights)):
|
||||
ui.progress_bar(index, count, step=1, text="Drawing buildings")
|
||||
fill: Color()
|
||||
for way in constructor.buildings:
|
||||
if way.height < height or way.min_height > height:
|
||||
for building in constructor.buildings:
|
||||
if building.height < height or building.min_height > height:
|
||||
continue
|
||||
shift_1 = [0, -previous_height * scale]
|
||||
shift_2 = [0, -height * scale]
|
||||
for segment in way.parts:
|
||||
if height == 2:
|
||||
fill = Color("#AAAAAA")
|
||||
elif height == 4:
|
||||
fill = Color("#C3C3C3")
|
||||
else:
|
||||
color_part: float = 0.8 + segment.angle * 0.2
|
||||
fill = Color(rgb=(color_part, color_part, color_part))
|
||||
building.draw_walls(self.svg, height, previous_height, scale)
|
||||
|
||||
command = (
|
||||
"M",
|
||||
segment.point_1 + shift_1,
|
||||
"L",
|
||||
segment.point_2 + shift_1,
|
||||
segment.point_2 + shift_2,
|
||||
segment.point_1 + shift_2,
|
||||
segment.point_1 + shift_1,
|
||||
"Z",
|
||||
)
|
||||
path = self.svg.path(
|
||||
d=command,
|
||||
fill=fill.hex,
|
||||
stroke=fill.hex,
|
||||
stroke_width=1,
|
||||
stroke_linejoin="round",
|
||||
)
|
||||
self.svg.add(path)
|
||||
|
||||
# Draw building roofs.
|
||||
|
||||
for way in constructor.buildings:
|
||||
if way.height == height:
|
||||
shift = np.array([0, -way.height * scale])
|
||||
path_commands: str = way.get_path(self.flinger, shift)
|
||||
path = Path(d=path_commands, opacity=1)
|
||||
path.update(way.line_style.style)
|
||||
path.update({"stroke-linejoin": "round"})
|
||||
self.svg.add(path)
|
||||
for building in constructor.buildings:
|
||||
if building.height == height:
|
||||
building.draw_roof(self.svg, self.flinger, scale)
|
||||
|
||||
previous_height = height
|
||||
|
||||
ui.progress_bar(-1, count, step=1, text="Drawing buildings")
|
||||
|
||||
def draw_direction(self, constructor) -> None:
|
||||
"""
|
||||
Draw gradient sectors for directions.
|
||||
"""
|
||||
for node in constructor.points:
|
||||
|
||||
angle = None
|
||||
is_revert_gradient: bool = False
|
||||
|
||||
if node.get_tag("man_made") == "surveillance":
|
||||
direction = node.get_tag("camera:direction")
|
||||
if "camera:angle" in node.tags:
|
||||
angle = float(node.get_tag("camera:angle"))
|
||||
if "angle" in node.tags:
|
||||
angle = float(node.get_tag("angle"))
|
||||
direction_radius: float = 25
|
||||
direction_color: Color = self.scheme.get_color(
|
||||
"direction_camera_color"
|
||||
)
|
||||
elif node.get_tag("traffic_sign") == "stop":
|
||||
direction = node.get_tag("direction")
|
||||
direction_radius: float = 25
|
||||
direction_color: Color = Color("red")
|
||||
else:
|
||||
direction = node.get_tag("direction")
|
||||
direction_radius: float = 50
|
||||
direction_color: Color = self.scheme.get_color(
|
||||
"direction_view_color"
|
||||
)
|
||||
is_revert_gradient = True
|
||||
|
||||
if not direction:
|
||||
continue
|
||||
|
||||
point = (node.point.astype(int)).astype(float)
|
||||
|
||||
if angle:
|
||||
paths = [Sector(direction, angle).draw(point, direction_radius)]
|
||||
else:
|
||||
paths = DirectionSet(direction).draw(point, direction_radius)
|
||||
|
||||
for path in paths:
|
||||
radial_gradient = self.svg.radialGradient(
|
||||
center=point,
|
||||
r=direction_radius,
|
||||
gradientUnits="userSpaceOnUse",
|
||||
)
|
||||
gradient = self.svg.defs.add(radial_gradient)
|
||||
if is_revert_gradient:
|
||||
(
|
||||
gradient
|
||||
.add_stop_color(0, direction_color.hex, opacity=0)
|
||||
.add_stop_color(1, direction_color.hex, opacity=0.7)
|
||||
) # fmt: skip
|
||||
else:
|
||||
(
|
||||
gradient
|
||||
.add_stop_color(0, direction_color.hex, opacity=0.4)
|
||||
.add_stop_color(1, direction_color.hex, opacity=0)
|
||||
) # fmt: skip
|
||||
path = self.svg.path(
|
||||
d=["M", point] + path + ["L", point, "Z"],
|
||||
fill=gradient.get_paint_server(),
|
||||
)
|
||||
self.svg.add(path)
|
||||
|
||||
def draw_road(
|
||||
self, road: Road, color: Color, extra_width: float = 0
|
||||
) -> None:
|
||||
|
@ -336,7 +158,7 @@ class Painter:
|
|||
scale = self.flinger.get_scale(road.outers[0][0].coordinates)
|
||||
path_commands: str = road.get_path(self.flinger)
|
||||
path = Path(d=path_commands)
|
||||
style: Dict[str, Any] = {
|
||||
style: dict[str, Any] = {
|
||||
"fill": "none",
|
||||
"stroke": color.hex,
|
||||
"stroke-linecap": "round",
|
||||
|
@ -350,7 +172,7 @@ class Painter:
|
|||
"""
|
||||
Draw road as simple SVG path.
|
||||
"""
|
||||
nodes: Dict[OSMNode, Set[RoadPart]] = {}
|
||||
nodes: dict[OSMNode, set[RoadPart]] = {}
|
||||
|
||||
for road in roads:
|
||||
for index in range(len(road.outers[0]) - 1):
|
||||
|
@ -379,7 +201,7 @@ class Painter:
|
|||
intersection.draw(self.svg, scale, True)
|
||||
|
||||
|
||||
def check_level_number(tags: Dict[str, Any], level: float):
|
||||
def check_level_number(tags: dict[str, Any], level: float):
|
||||
"""
|
||||
Check if element described by tags is no the specified level.
|
||||
"""
|
||||
|
@ -392,7 +214,7 @@ def check_level_number(tags: Dict[str, Any], level: float):
|
|||
return True
|
||||
|
||||
|
||||
def check_level_overground(tags: Dict[str, Any]) -> bool:
|
||||
def check_level_overground(tags: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if element described by tags is overground.
|
||||
"""
|
||||
|
|
|
@ -4,7 +4,7 @@ Moire markup extension for Röntgen.
|
|||
import argparse
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Union
|
||||
|
||||
import yaml
|
||||
from moire.default import Default, DefaultHTML, DefaultMarkdown, DefaultWiki
|
||||
|
@ -17,8 +17,8 @@ from roentgen.workspace import workspace
|
|||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
Arguments = List[Any]
|
||||
Code = Union[str, Tag, List]
|
||||
Arguments = list[Any]
|
||||
Code = Union[str, Tag, list]
|
||||
|
||||
PREFIX: str = "https://wiki.openstreetmap.org/wiki/"
|
||||
|
||||
|
@ -29,13 +29,13 @@ class ArgumentParser(argparse.ArgumentParser):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.arguments: List[Dict[str, Any]] = []
|
||||
self.arguments: list[dict[str, Any]] = []
|
||||
super(ArgumentParser, self).__init__(*args, **kwargs)
|
||||
|
||||
def add_argument(self, *args, **kwargs) -> None:
|
||||
"""Just store argument with options."""
|
||||
super(ArgumentParser, self).add_argument(*args, **kwargs)
|
||||
argument: Dict[str, Any] = {"arguments": [x for x in args]}
|
||||
argument: dict[str, Any] = {"arguments": [x for x in args]}
|
||||
|
||||
for key in kwargs:
|
||||
argument[key] = kwargs[key]
|
||||
|
@ -57,7 +57,7 @@ class ArgumentParser(argparse.ArgumentParser):
|
|||
row: Code = [[x for y in array for x in y][:-1]]
|
||||
|
||||
if "help" in option:
|
||||
help_value: List = [option["help"]]
|
||||
help_value: list = [option["help"]]
|
||||
if (
|
||||
"default" in option
|
||||
and option["default"]
|
||||
|
@ -91,13 +91,13 @@ class TestConfiguration:
|
|||
"""
|
||||
|
||||
def __init__(self, test_config: Path):
|
||||
self.steps: Dict[str, Any] = {}
|
||||
self.steps: dict[str, Any] = {}
|
||||
|
||||
with test_config.open() as input_file:
|
||||
content: Dict[str, Any] = yaml.load(
|
||||
content: dict[str, Any] = yaml.load(
|
||||
input_file, Loader=yaml.FullLoader
|
||||
)
|
||||
steps: List[Dict[str, Any]] = content["jobs"]["build"]["steps"]
|
||||
steps: list[dict[str, Any]] = content["jobs"]["build"]["steps"]
|
||||
for step in steps:
|
||||
if "name" not in step:
|
||||
continue
|
||||
|
@ -177,7 +177,8 @@ class RoentgenHTML(RoentgenMoire, DefaultHTML):
|
|||
Simple HTML.
|
||||
"""
|
||||
|
||||
images = {}
|
||||
def __init__(self):
|
||||
self.images: dict = {}
|
||||
|
||||
def color(self, args: Arguments) -> str:
|
||||
"""Simple color sample."""
|
||||
|
@ -202,10 +203,11 @@ class RoentgenOSMWiki(RoentgenMoire, DefaultWiki):
|
|||
See https://wiki.openstreetmap.org/wiki/Main_Page
|
||||
"""
|
||||
|
||||
images = {}
|
||||
extractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
def __init__(self):
|
||||
self.images: dict = {}
|
||||
self.extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
|
||||
def osm(self, args: Arguments) -> str:
|
||||
"""OSM tag key or key–value pair of tag."""
|
||||
|
|
|
@ -4,8 +4,8 @@ Getting OpenStreetMap data from the web.
|
|||
import logging
|
||||
import time
|
||||
import urllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import urllib3
|
||||
|
||||
|
@ -14,44 +14,44 @@ from roentgen.ui import BoundaryBox
|
|||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
SLEEP_TIME_BETWEEN_REQUESTS: float = 2.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkError(Exception):
|
||||
"""Failed network request."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__()
|
||||
self.message: str = message
|
||||
message: str
|
||||
|
||||
|
||||
def get_osm(
|
||||
boundary_box: BoundaryBox, cache_path: Path, to_update: bool = False
|
||||
boundary_box: BoundaryBox, cache_file_path: Path, to_update: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Download OSM data from the web or get if from the cache.
|
||||
|
||||
:param boundary_box: borders of the map part to download
|
||||
:param cache_path: cache directory to store downloaded OSM files
|
||||
:param cache_file_path: cache file to store downloaded OSM data
|
||||
:param to_update: update cache files
|
||||
"""
|
||||
result_file_name: Path = cache_path / f"{boundary_box.get_format()}.osm"
|
||||
if not to_update and cache_file_path.is_file():
|
||||
with cache_file_path.open() as output_file:
|
||||
return output_file.read()
|
||||
|
||||
if not to_update and result_file_name.is_file():
|
||||
return result_file_name.open().read()
|
||||
|
||||
content: Optional[str] = get_data(
|
||||
content: str = get_data(
|
||||
"api.openstreetmap.org/api/0.6/map",
|
||||
{"bbox": boundary_box.get_format()},
|
||||
is_secure=True,
|
||||
).decode("utf-8")
|
||||
|
||||
with result_file_name.open("w+") as output_file:
|
||||
with cache_file_path.open("w+") as output_file:
|
||||
output_file.write(content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def get_data(
|
||||
address: str, parameters: Dict[str, str], is_secure: bool = False
|
||||
address: str, parameters: dict[str, str], is_secure: bool = False
|
||||
) -> bytes:
|
||||
"""
|
||||
Construct Internet page URL and get its descriptor.
|
||||
|
@ -61,7 +61,7 @@ def get_data(
|
|||
:param is_secure: https or http
|
||||
:return: connection descriptor
|
||||
"""
|
||||
url: str = "http" + ("s" if is_secure else "") + "://" + address
|
||||
url: str = f"http{('s' if is_secure else '')}://{address}"
|
||||
if len(parameters) > 0:
|
||||
url += f"?{urllib.parse.urlencode(parameters)}"
|
||||
logging.info(f"Getting {url}...")
|
||||
|
@ -74,5 +74,5 @@ def get_data(
|
|||
raise NetworkError("Cannot download data: too many attempts.")
|
||||
|
||||
pool_manager.clear()
|
||||
time.sleep(2)
|
||||
time.sleep(SLEEP_TIME_BETWEEN_REQUESTS)
|
||||
return result.data
|
||||
|
|
|
@ -6,7 +6,7 @@ import re
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any, Optional
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import numpy as np
|
||||
|
@ -23,7 +23,8 @@ KILOMETERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$")
|
|||
MILES_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$")
|
||||
|
||||
|
||||
STAGES_OF_DECAY: List[str] = [
|
||||
# See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay
|
||||
STAGES_OF_DECAY: list[str] = [
|
||||
"disused",
|
||||
"abandoned",
|
||||
"ruins",
|
||||
|
@ -50,8 +51,9 @@ class Tagged:
|
|||
OpenStreetMap element (node, way or relation) with tags.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.tags: Dict[str, str] = {}
|
||||
def __init__(self, tags: dict[str, str] = None):
|
||||
self.tags: dict[str, str]
|
||||
self.tags = {} if tags is None else tags
|
||||
|
||||
def get_tag(self, key: str) -> Optional[str]:
|
||||
"""
|
||||
|
@ -139,7 +141,7 @@ class OSMNode(Tagged):
|
|||
node.tags[subattributes["k"]] = subattributes["v"]
|
||||
return node
|
||||
|
||||
def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMNode":
|
||||
def parse_from_structure(self, structure: dict[str, Any]) -> "OSMNode":
|
||||
"""
|
||||
Parse node from Overpass-like structure.
|
||||
|
||||
|
@ -160,11 +162,11 @@ class OSMWay(Tagged):
|
|||
See https://wiki.openstreetmap.org/wiki/Way
|
||||
"""
|
||||
|
||||
def __init__(self, id_: int = 0, nodes: Optional[List[OSMNode]] = None):
|
||||
def __init__(self, id_: int = 0, nodes: Optional[list[OSMNode]] = None):
|
||||
super().__init__()
|
||||
|
||||
self.id_: int = id_
|
||||
self.nodes: List[OSMNode] = [] if nodes is None else nodes
|
||||
self.nodes: list[OSMNode] = [] if nodes is None else nodes
|
||||
|
||||
self.visible: Optional[str] = None
|
||||
self.changeset: Optional[str] = None
|
||||
|
@ -194,7 +196,7 @@ class OSMWay(Tagged):
|
|||
return way
|
||||
|
||||
def parse_from_structure(
|
||||
self, structure: Dict[str, Any], nodes
|
||||
self, structure: dict[str, Any], nodes
|
||||
) -> "OSMWay":
|
||||
"""
|
||||
Parse way from Overpass-like structure.
|
||||
|
@ -245,7 +247,7 @@ class OSMRelation(Tagged):
|
|||
super().__init__()
|
||||
|
||||
self.id_: int = id_
|
||||
self.members: List["OSMMember"] = []
|
||||
self.members: list["OSMMember"] = []
|
||||
self.user: Optional[str] = None
|
||||
self.timestamp: Optional[datetime] = None
|
||||
|
||||
|
@ -275,7 +277,7 @@ class OSMRelation(Tagged):
|
|||
relation.tags[subelement.attrib["k"]] = subelement.attrib["v"]
|
||||
return relation
|
||||
|
||||
def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMRelation":
|
||||
def parse_from_structure(self, structure: dict[str, Any]) -> "OSMRelation":
|
||||
"""
|
||||
Parse relation from Overpass-like structure.
|
||||
|
||||
|
@ -305,19 +307,19 @@ class OSMMember:
|
|||
role: str = ""
|
||||
|
||||
|
||||
class Map:
|
||||
class OSMData:
|
||||
"""
|
||||
The whole OpenStreetMap information about nodes, ways, and relations.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes: Dict[int, OSMNode] = {}
|
||||
self.ways: Dict[int, OSMWay] = {}
|
||||
self.relations: Dict[int, OSMRelation] = {}
|
||||
self.nodes: dict[int, OSMNode] = {}
|
||||
self.ways: dict[int, OSMWay] = {}
|
||||
self.relations: dict[int, OSMRelation] = {}
|
||||
|
||||
self.authors: Set[str] = set()
|
||||
self.authors: set[str] = set()
|
||||
self.time: MinMax = MinMax()
|
||||
self.boundary_box: List[MinMax] = [MinMax(), MinMax()]
|
||||
self.boundary_box: list[MinMax] = [MinMax(), MinMax()]
|
||||
self.view_box = None
|
||||
|
||||
def add_node(self, node: OSMNode) -> None:
|
||||
|
@ -355,9 +357,9 @@ class OverpassReader:
|
|||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.map_ = Map()
|
||||
self.osm_data = OSMData()
|
||||
|
||||
def parse_json_file(self, file_name: Path) -> Map:
|
||||
def parse_json_file(self, file_name: Path) -> OSMData:
|
||||
"""
|
||||
Parse JSON structure from the file and construct map.
|
||||
"""
|
||||
|
@ -371,18 +373,18 @@ class OverpassReader:
|
|||
if element["type"] == "node":
|
||||
node = OSMNode().parse_from_structure(element)
|
||||
node_map[node.id_] = node
|
||||
self.map_.add_node(node)
|
||||
self.osm_data.add_node(node)
|
||||
for element in structure["elements"]:
|
||||
if element["type"] == "way":
|
||||
way = OSMWay().parse_from_structure(element, node_map)
|
||||
way_map[way.id_] = way
|
||||
self.map_.add_way(way)
|
||||
self.osm_data.add_way(way)
|
||||
for element in structure["elements"]:
|
||||
if element["type"] == "relation":
|
||||
relation = OSMRelation().parse_from_structure(element)
|
||||
self.map_.add_relation(relation)
|
||||
self.osm_data.add_relation(relation)
|
||||
|
||||
return self.map_
|
||||
return self.osm_data
|
||||
|
||||
|
||||
class OSMReader:
|
||||
|
@ -406,13 +408,13 @@ class OSMReader:
|
|||
:param is_full: whether metadata should be parsed: tags `visible`,
|
||||
`changeset`, `timestamp`, `user`, `uid`
|
||||
"""
|
||||
self.map_ = Map()
|
||||
self.osm_data = OSMData()
|
||||
self.parse_nodes: bool = parse_nodes
|
||||
self.parse_ways: bool = parse_ways
|
||||
self.parse_relations: bool = parse_relations
|
||||
self.is_full: bool = is_full
|
||||
|
||||
def parse_osm_file(self, file_name: Path) -> Map:
|
||||
def parse_osm_file(self, file_name: Path) -> OSMData:
|
||||
"""
|
||||
Parse OSM XML file.
|
||||
|
||||
|
@ -421,7 +423,7 @@ class OSMReader:
|
|||
"""
|
||||
return self.parse_osm(ElementTree.parse(file_name).getroot())
|
||||
|
||||
def parse_osm_text(self, text: str) -> Map:
|
||||
def parse_osm_text(self, text: str) -> OSMData:
|
||||
"""
|
||||
Parse OSM XML data from text representation.
|
||||
|
||||
|
@ -430,7 +432,7 @@ class OSMReader:
|
|||
"""
|
||||
return self.parse_osm(ElementTree.fromstring(text))
|
||||
|
||||
def parse_osm(self, root) -> Map:
|
||||
def parse_osm(self, root) -> OSMData:
|
||||
"""
|
||||
Parse OSM XML data.
|
||||
|
||||
|
@ -442,25 +444,25 @@ class OSMReader:
|
|||
self.parse_bounds(element)
|
||||
if element.tag == "node" and self.parse_nodes:
|
||||
node = OSMNode.from_xml_structure(element, self.is_full)
|
||||
self.map_.add_node(node)
|
||||
self.osm_data.add_node(node)
|
||||
if element.tag == "way" and self.parse_ways:
|
||||
self.map_.add_way(
|
||||
self.osm_data.add_way(
|
||||
OSMWay.from_xml_structure(
|
||||
element, self.map_.nodes, self.is_full
|
||||
element, self.osm_data.nodes, self.is_full
|
||||
)
|
||||
)
|
||||
if element.tag == "relation" and self.parse_relations:
|
||||
self.map_.add_relation(
|
||||
self.osm_data.add_relation(
|
||||
OSMRelation.from_xml_structure(element, self.is_full)
|
||||
)
|
||||
return self.map_
|
||||
return self.osm_data
|
||||
|
||||
def parse_bounds(self, element) -> None:
|
||||
"""
|
||||
Parse view box from XML element.
|
||||
"""
|
||||
attributes = element.attrib
|
||||
self.map_.view_box = MinMax(
|
||||
self.osm_data.view_box = MinMax(
|
||||
np.array(
|
||||
(float(attributes["minlat"]), float(attributes["minlon"]))
|
||||
),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Point: node representation on the map.
|
||||
"""
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
|
@ -56,9 +56,9 @@ class Point(Tagged):
|
|||
def __init__(
|
||||
self,
|
||||
icon_set: IconSet,
|
||||
labels: List[Label],
|
||||
tags: Dict[str, str],
|
||||
processed: Set[str],
|
||||
labels: list[Label],
|
||||
tags: dict[str, str],
|
||||
processed: set[str],
|
||||
point: np.array,
|
||||
coordinates: np.array,
|
||||
priority: float = 0,
|
||||
|
@ -70,9 +70,9 @@ class Point(Tagged):
|
|||
assert point is not None
|
||||
|
||||
self.icon_set: IconSet = icon_set
|
||||
self.labels: List[Label] = labels
|
||||
self.tags: Dict[str, str] = tags
|
||||
self.processed: Set[str] = processed
|
||||
self.labels: list[Label] = labels
|
||||
self.tags: dict[str, str] = tags
|
||||
self.processed: set[str] = processed
|
||||
self.point: np.array = point
|
||||
self.coordinates: np.array = coordinates
|
||||
self.priority: float = priority
|
||||
|
@ -139,7 +139,7 @@ class Point(Tagged):
|
|||
icon: Icon,
|
||||
position,
|
||||
occupied,
|
||||
tags: Optional[Dict[str, str]] = None,
|
||||
tags: Optional[dict[str, str]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Draw one combined icon and its outline.
|
||||
|
@ -176,7 +176,7 @@ class Point(Tagged):
|
|||
"""
|
||||
Draw all labels.
|
||||
"""
|
||||
labels: List[Label]
|
||||
labels: list[Label]
|
||||
|
||||
if label_mode == "main":
|
||||
labels = self.labels[:1]
|
||||
|
|
|
@ -5,7 +5,6 @@ import logging
|
|||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
@ -25,7 +24,7 @@ def rasterize(from_: Path, to_: Path, area: str = "", dpi: float = 90) -> None:
|
|||
f"Inkscape and set the variable to be able to rasterize SVG files."
|
||||
)
|
||||
|
||||
commands: List[str] = [os.environ[INKSCAPE_BIN]]
|
||||
commands: list[str] = [os.environ[INKSCAPE_BIN]]
|
||||
commands += ["--export-png", to_.absolute()]
|
||||
commands += ["--export-dpi", str(dpi)]
|
||||
if area:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
WIP: road shape drawing.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
|
@ -48,7 +48,7 @@ class RoadPart:
|
|||
self,
|
||||
point_1: np.array,
|
||||
point_2: np.array,
|
||||
lanes: List[Lane],
|
||||
lanes: list[Lane],
|
||||
scale: False,
|
||||
):
|
||||
"""
|
||||
|
@ -58,7 +58,7 @@ class RoadPart:
|
|||
"""
|
||||
self.point_1: np.array = point_1
|
||||
self.point_2: np.array = point_2
|
||||
self.lanes: List[Lane] = lanes
|
||||
self.lanes: list[Lane] = lanes
|
||||
if lanes:
|
||||
self.width = sum(map(lambda x: x.get_width(scale), lanes))
|
||||
else:
|
||||
|
@ -297,8 +297,8 @@ class Intersection:
|
|||
points of the road parts should be the same.
|
||||
"""
|
||||
|
||||
def __init__(self, parts: List[RoadPart]):
|
||||
self.parts: List[RoadPart] = sorted(parts, key=lambda x: x.get_angle())
|
||||
def __init__(self, parts: list[RoadPart]):
|
||||
self.parts: list[RoadPart] = sorted(parts, key=lambda x: x.get_angle())
|
||||
|
||||
for index in range(len(self.parts)):
|
||||
next_index: int = 0 if index == len(self.parts) - 1 else index + 1
|
||||
|
|
|
@ -4,7 +4,7 @@ Röntgen drawing scheme.
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
import yaml
|
||||
from colour import Color
|
||||
|
@ -30,7 +30,7 @@ class LineStyle:
|
|||
SVG line style and its priority.
|
||||
"""
|
||||
|
||||
style: Dict[str, Union[int, float, str]]
|
||||
style: dict[str, Union[int, float, str]]
|
||||
priority: float = 0.0
|
||||
|
||||
|
||||
|
@ -48,7 +48,7 @@ class MatchingType(Enum):
|
|||
def is_matched_tag(
|
||||
matcher_tag_key: str,
|
||||
matcher_tag_value: Union[str, list],
|
||||
tags: Dict[str, str],
|
||||
tags: dict[str, str],
|
||||
) -> MatchingType:
|
||||
"""
|
||||
Check whether element tags contradict tag matcher.
|
||||
|
@ -89,10 +89,10 @@ class Matcher:
|
|||
Tag matching.
|
||||
"""
|
||||
|
||||
def __init__(self, structure: Dict[str, Any]):
|
||||
self.tags: Dict[str, str] = structure["tags"]
|
||||
def __init__(self, structure: dict[str, Any]):
|
||||
self.tags: dict[str, str] = structure["tags"]
|
||||
|
||||
self.exception: Dict[str, str] = {}
|
||||
self.exception: dict[str, str] = {}
|
||||
if "exception" in structure:
|
||||
self.exception = structure["exception"]
|
||||
|
||||
|
@ -100,11 +100,11 @@ class Matcher:
|
|||
if "replace_shapes" in structure:
|
||||
self.replace_shapes = structure["replace_shapes"]
|
||||
|
||||
self.location_restrictions: Dict[str, str] = {}
|
||||
self.location_restrictions: dict[str, str] = {}
|
||||
if "location_restrictions" in structure:
|
||||
self.location_restrictions = structure["location_restrictions"]
|
||||
|
||||
def is_matched(self, tags: Dict[str, str]) -> bool:
|
||||
def is_matched(self, tags: dict[str, str]) -> bool:
|
||||
"""
|
||||
Check whether element tags matches tag matcher.
|
||||
|
||||
|
@ -157,7 +157,7 @@ class NodeMatcher(Matcher):
|
|||
Tag specification matcher.
|
||||
"""
|
||||
|
||||
def __init__(self, structure: Dict[str, Any]):
|
||||
def __init__(self, structure: dict[str, Any]):
|
||||
# Dictionary with tag keys and values, value lists, or "*"
|
||||
super().__init__(structure)
|
||||
|
||||
|
@ -200,11 +200,11 @@ class WayMatcher(Matcher):
|
|||
Special tag matcher for ways.
|
||||
"""
|
||||
|
||||
def __init__(self, structure: Dict[str, Any], scheme: "Scheme"):
|
||||
def __init__(self, structure: dict[str, Any], scheme: "Scheme"):
|
||||
super().__init__(structure)
|
||||
self.style: Dict[str, Any] = {"fill": "none"}
|
||||
self.style: dict[str, Any] = {"fill": "none"}
|
||||
if "style" in structure:
|
||||
style: Dict[str, Any] = structure["style"]
|
||||
style: dict[str, Any] = structure["style"]
|
||||
for key in style:
|
||||
if str(style[key]).endswith("_color"):
|
||||
self.style[key] = scheme.get_color(style[key]).hex.upper()
|
||||
|
@ -223,7 +223,7 @@ class RoadMatcher(Matcher):
|
|||
Special tag matcher for highways.
|
||||
"""
|
||||
|
||||
def __init__(self, structure: Dict[str, Any], scheme: "Scheme"):
|
||||
def __init__(self, structure: dict[str, Any], scheme: "Scheme"):
|
||||
super().__init__(structure)
|
||||
self.border_color: Color = Color(
|
||||
scheme.get_color(structure["border_color"])
|
||||
|
@ -250,33 +250,33 @@ class Scheme:
|
|||
specification
|
||||
"""
|
||||
with file_name.open() as input_file:
|
||||
content: Dict[str, Any] = yaml.load(
|
||||
content: dict[str, Any] = yaml.load(
|
||||
input_file.read(), Loader=yaml.FullLoader
|
||||
)
|
||||
self.node_matchers: List[NodeMatcher] = []
|
||||
self.node_matchers: list[NodeMatcher] = []
|
||||
for group in content["node_icons"]:
|
||||
for element in group["tags"]:
|
||||
self.node_matchers.append(NodeMatcher(element))
|
||||
|
||||
self.colors: Dict[str, str] = content["colors"]
|
||||
self.material_colors: Dict[str, str] = content["material_colors"]
|
||||
self.colors: dict[str, str] = content["colors"]
|
||||
self.material_colors: dict[str, str] = content["material_colors"]
|
||||
|
||||
self.way_matchers: List[WayMatcher] = [
|
||||
self.way_matchers: list[WayMatcher] = [
|
||||
WayMatcher(x, self) for x in content["ways"]
|
||||
]
|
||||
self.road_matchers: List[RoadMatcher] = [
|
||||
self.road_matchers: list[RoadMatcher] = [
|
||||
RoadMatcher(x, self) for x in content["roads"]
|
||||
]
|
||||
self.area_matchers: List[Matcher] = [
|
||||
self.area_matchers: list[Matcher] = [
|
||||
Matcher(x) for x in content["area_tags"]
|
||||
]
|
||||
self.tags_to_write: List[str] = content["tags_to_write"]
|
||||
self.prefix_to_write: List[str] = content["prefix_to_write"]
|
||||
self.tags_to_skip: List[str] = content["tags_to_skip"]
|
||||
self.prefix_to_skip: List[str] = content["prefix_to_skip"]
|
||||
self.tags_to_write: list[str] = content["tags_to_write"]
|
||||
self.prefix_to_write: list[str] = content["prefix_to_write"]
|
||||
self.tags_to_skip: list[str] = content["tags_to_skip"]
|
||||
self.prefix_to_skip: list[str] = content["prefix_to_skip"]
|
||||
|
||||
# Storage for created icon sets.
|
||||
self.cache: Dict[str, Tuple[IconSet, int]] = {}
|
||||
self.cache: dict[str, Tuple[IconSet, int]] = {}
|
||||
|
||||
def get_color(self, color: str) -> Color:
|
||||
"""
|
||||
|
@ -327,8 +327,8 @@ class Scheme:
|
|||
def get_icon(
|
||||
self,
|
||||
extractor: ShapeExtractor,
|
||||
tags: Dict[str, Any],
|
||||
processed: Set[str],
|
||||
tags: dict[str, Any],
|
||||
processed: set[str],
|
||||
for_: str = "node",
|
||||
) -> Tuple[IconSet, int]:
|
||||
"""
|
||||
|
@ -347,7 +347,7 @@ class Scheme:
|
|||
return self.cache[tags_hash]
|
||||
|
||||
main_icon: Optional[Icon] = None
|
||||
extra_icons: List[Icon] = []
|
||||
extra_icons: list[Icon] = []
|
||||
priority: int = 0
|
||||
|
||||
index: int = 0
|
||||
|
@ -358,7 +358,7 @@ class Scheme:
|
|||
matched: bool = matcher.is_matched(tags)
|
||||
if not matched:
|
||||
continue
|
||||
matcher_tags: Set[str] = set(matcher.tags.keys())
|
||||
matcher_tags: set[str] = set(matcher.tags.keys())
|
||||
priority = len(self.node_matchers) - index
|
||||
if not matcher.draw:
|
||||
processed |= matcher_tags
|
||||
|
@ -437,7 +437,7 @@ class Scheme:
|
|||
|
||||
return returned, priority
|
||||
|
||||
def get_style(self, tags: Dict[str, Any], scale):
|
||||
def get_style(self, tags: dict[str, Any], scale):
|
||||
"""
|
||||
Get line style based on tags and scale.
|
||||
"""
|
||||
|
@ -451,7 +451,7 @@ class Scheme:
|
|||
|
||||
return line_styles
|
||||
|
||||
def get_road(self, tags: Dict[str, Any]) -> Optional[RoadMatcher]:
|
||||
def get_road(self, tags: dict[str, Any]) -> Optional[RoadMatcher]:
|
||||
for matcher in self.road_matchers:
|
||||
if not matcher.is_matched(tags):
|
||||
continue
|
||||
|
@ -459,12 +459,12 @@ class Scheme:
|
|||
return None
|
||||
|
||||
def construct_text(
|
||||
self, tags: Dict[str, str], draw_captions: str, processed: Set[str]
|
||||
) -> List[Label]:
|
||||
self, tags: dict[str, str], draw_captions: str, processed: set[str]
|
||||
) -> list[Label]:
|
||||
"""
|
||||
Construct labels for not processed tags.
|
||||
"""
|
||||
texts: List[Label] = []
|
||||
texts: list[Label] = []
|
||||
|
||||
name = None
|
||||
alt_name = None
|
||||
|
@ -490,7 +490,7 @@ class Scheme:
|
|||
alt_name = ""
|
||||
alt_name += "ex " + tags["old_name"]
|
||||
|
||||
address: List[str] = get_address(tags, draw_captions, processed)
|
||||
address: list[str] = get_address(tags, draw_captions, processed)
|
||||
|
||||
if name:
|
||||
texts.append(Label(name, Color("black")))
|
||||
|
@ -535,7 +535,7 @@ class Scheme:
|
|||
texts.append(Label(tags[tag]))
|
||||
return texts
|
||||
|
||||
def is_area(self, tags: Dict[str, str]) -> bool:
|
||||
def is_area(self, tags: dict[str, str]) -> bool:
|
||||
"""
|
||||
Check whether way described by tags is area.
|
||||
"""
|
||||
|
@ -545,7 +545,7 @@ class Scheme:
|
|||
return False
|
||||
|
||||
def process_ignored(
|
||||
self, tags: Dict[str, str], processed: Set[str]
|
||||
self, tags: dict[str, str], processed: set[str]
|
||||
) -> None:
|
||||
"""
|
||||
Mark all ignored tag as processed.
|
||||
|
@ -553,6 +553,4 @@ class Scheme:
|
|||
:param tags: input tag dictionary
|
||||
:param processed: processed set
|
||||
"""
|
||||
for tag in tags:
|
||||
if self.is_no_drawable(tag):
|
||||
processed.add(tag)
|
||||
[processed.add(tag) for tag in tags if self.is_no_drawable(tag)]
|
||||
|
|
|
@ -12,26 +12,23 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
update_cache: bool = False
|
||||
"""
|
||||
HTTP request handler that process sloppy map tile requests.
|
||||
"""
|
||||
|
||||
def __init__(self, request, client_address, server):
|
||||
super().__init__(request, client_address, server)
|
||||
self.cache: Path = Path("cache")
|
||||
self.update_cache: bool = False
|
||||
|
||||
def write(self, message):
|
||||
if isinstance(message, bytes):
|
||||
self.wfile.write(message)
|
||||
else:
|
||||
self.wfile.write(message.encode("utf-8"))
|
||||
|
||||
def do_GET(self):
|
||||
parts = self.path.split("/")
|
||||
def do_GET(self) -> None:
|
||||
"""Serve a GET request."""
|
||||
parts: list[str] = self.path.split("/")
|
||||
if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"):
|
||||
return
|
||||
zoom = int(parts[2])
|
||||
x = int(parts[3])
|
||||
y = int(parts[4])
|
||||
zoom: int = int(parts[2])
|
||||
x: int = int(parts[3])
|
||||
y: int = int(parts[4])
|
||||
tile_path: Path = workspace.get_tile_path()
|
||||
png_path = tile_path / f"tile_{zoom}_{x}_{y}.png"
|
||||
if self.update_cache:
|
||||
|
@ -48,7 +45,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||
self.send_response(200)
|
||||
self.send_header("Content-type", "image/png")
|
||||
self.end_headers()
|
||||
self.write(input_file.read())
|
||||
self.wfile.write(input_file.read())
|
||||
return
|
||||
|
||||
|
||||
|
@ -56,7 +53,7 @@ def ui(options):
|
|||
server: Optional[HTTPServer] = None
|
||||
try:
|
||||
port: int = 8080
|
||||
server = HTTPServer(("", port), Handler)
|
||||
server: HTTPServer = HTTPServer(("", port), Handler)
|
||||
server.cache_path = Path(options.cache)
|
||||
server.serve_forever()
|
||||
logging.info(f"Server started on port {port}.")
|
||||
|
|
|
@ -7,7 +7,6 @@ import json
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from roentgen import (
|
||||
__author__,
|
||||
|
@ -55,7 +54,7 @@ class TaginfoProjectFile:
|
|||
):
|
||||
key: str = list(matcher.tags.keys())[0]
|
||||
value: str = matcher.tags[key]
|
||||
ids: List[str] = [
|
||||
ids: list[str] = [
|
||||
(x if isinstance(x, str) else x["shape"])
|
||||
for x in matcher.shapes
|
||||
]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
OSM address tag processing.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Set
|
||||
from typing import Any
|
||||
|
||||
from colour import Color
|
||||
|
||||
|
@ -24,15 +24,15 @@ class Label:
|
|||
|
||||
|
||||
def get_address(
|
||||
tags: Dict[str, Any], draw_captions_mode: str, processed: Set[str]
|
||||
) -> List[str]:
|
||||
tags: dict[str, Any], draw_captions_mode: str, processed: set[str]
|
||||
) -> list[str]:
|
||||
"""
|
||||
Construct address text list from the tags.
|
||||
|
||||
:param tags: OSM node, way or relation tags
|
||||
:param draw_captions_mode: captions mode ("all", "main", or "no")
|
||||
"""
|
||||
address: List[str] = []
|
||||
address: list[str] = []
|
||||
|
||||
if draw_captions_mode == "address":
|
||||
if "addr:postcode" in tags:
|
||||
|
@ -80,12 +80,12 @@ def format_frequency(value: str) -> str:
|
|||
return f"{value} "
|
||||
|
||||
|
||||
def get_text(tags: Dict[str, Any], processed: Set[str]) -> List[Label]:
|
||||
def get_text(tags: dict[str, Any], processed: set[str]) -> list[Label]:
|
||||
"""
|
||||
Get text representation of writable tags.
|
||||
"""
|
||||
texts: List[Label] = []
|
||||
values: List[str] = []
|
||||
texts: list[Label] = []
|
||||
values: list[str] = []
|
||||
|
||||
if "voltage:primary" in tags:
|
||||
values.append(tags["voltage:primary"])
|
||||
|
|
|
@ -7,7 +7,7 @@ import logging
|
|||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
|
@ -15,9 +15,9 @@ import svgwrite
|
|||
from roentgen.constructor import Constructor
|
||||
from roentgen.flinger import Flinger
|
||||
from roentgen.icon import ShapeExtractor
|
||||
from roentgen.mapper import Painter
|
||||
from roentgen.mapper import Map
|
||||
from roentgen.osm_getter import NetworkError, get_osm
|
||||
from roentgen.osm_reader import Map, OSMReader
|
||||
from roentgen.osm_reader import OSMData, OSMReader
|
||||
from roentgen.raster import rasterize
|
||||
from roentgen.scheme import Scheme
|
||||
from roentgen.ui import BoundaryBox
|
||||
|
@ -34,7 +34,7 @@ class Tiles:
|
|||
Collection of tiles.
|
||||
"""
|
||||
|
||||
tiles: List["Tile"]
|
||||
tiles: list["Tile"]
|
||||
tile_1: "Tile"
|
||||
tile_2: "Tile"
|
||||
scale: int
|
||||
|
@ -43,7 +43,7 @@ class Tiles:
|
|||
@classmethod
|
||||
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
|
||||
"""Create minimal set of tiles that cover boundary box."""
|
||||
tiles: List["Tile"] = []
|
||||
tiles: list["Tile"] = []
|
||||
tile_1 = Tile.from_coordinates(boundary_box.get_left_top(), scale)
|
||||
tile_2 = Tile.from_coordinates(boundary_box.get_right_bottom(), scale)
|
||||
|
||||
|
@ -69,15 +69,16 @@ class Tiles:
|
|||
:param directory: directory for tiles
|
||||
:param cache_path: directory for temporary OSM files
|
||||
"""
|
||||
get_osm(self.boundary_box, cache_path)
|
||||
|
||||
map_ = OSMReader().parse_osm_file(
|
||||
cache_path / (self.boundary_box.get_format() + ".osm")
|
||||
cache_file_path: Path = (
|
||||
cache_path / f"{self.boundary_box.get_format()}.osm"
|
||||
)
|
||||
get_osm(self.boundary_box, cache_file_path)
|
||||
|
||||
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
|
||||
for tile in self.tiles:
|
||||
file_path: Path = tile.get_file_name(directory)
|
||||
if not file_path.exists():
|
||||
tile.draw_for_map(map_, directory)
|
||||
tile.draw_for_map(osm_data, directory)
|
||||
else:
|
||||
logging.info(f"File {file_path} already exists.")
|
||||
|
||||
|
@ -97,14 +98,12 @@ class Tiles:
|
|||
self.boundary_box.get_format() + ".svg"
|
||||
)
|
||||
if not output_path.exists():
|
||||
content = get_osm(self.boundary_box, cache_path)
|
||||
if not content:
|
||||
logging.error("Cannot download OSM data.")
|
||||
return None
|
||||
|
||||
map_: Map = OSMReader().parse_osm_file(
|
||||
cache_path / (self.boundary_box.get_format() + ".osm")
|
||||
cache_file_path: Path = (
|
||||
cache_path / f"{self.boundary_box.get_format()}.osm"
|
||||
)
|
||||
get_osm(self.boundary_box, cache_file_path)
|
||||
|
||||
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
|
||||
lat_2, lon_1 = self.tile_1.get_coordinates()
|
||||
lat_1, lon_2 = Tile(
|
||||
self.tile_2.x + 1, self.tile_2.y + 1, self.scale
|
||||
|
@ -113,26 +112,20 @@ class Tiles:
|
|||
max_ = np.array((lat_2, lon_2))
|
||||
|
||||
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
|
||||
icon_extractor: ShapeExtractor = ShapeExtractor(
|
||||
extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
constructor: Constructor = Constructor(
|
||||
map_, flinger, scheme, icon_extractor
|
||||
osm_data, flinger, scheme, extractor
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||
str(output_path), size=flinger.size
|
||||
)
|
||||
painter: Painter = Painter(
|
||||
map_=map_,
|
||||
flinger=flinger,
|
||||
svg=svg,
|
||||
icon_extractor=icon_extractor,
|
||||
scheme=scheme,
|
||||
)
|
||||
painter.draw(constructor)
|
||||
map_: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
|
||||
map_.draw(constructor)
|
||||
|
||||
logging.info(f"Writing output SVG {output_path}...")
|
||||
with output_path.open("w+") as output_file:
|
||||
|
@ -204,18 +197,18 @@ class Tile:
|
|||
point_1[1], point_2[0], point_2[1], point_1[0]
|
||||
).round()
|
||||
|
||||
def load_map(self, cache_path: Path) -> Map:
|
||||
def load_osm_data(self, cache_path: Path) -> OSMData:
|
||||
"""
|
||||
Construct map data from extended boundary box.
|
||||
|
||||
:param cache_path: directory to store OSM data files
|
||||
"""
|
||||
boundary_box: BoundaryBox = self.get_extended_boundary_box()
|
||||
get_osm(boundary_box, cache_path)
|
||||
|
||||
return OSMReader().parse_osm_file(
|
||||
cache_path / f"{boundary_box.get_format()}.osm"
|
||||
cache_file_path: Path = (
|
||||
cache_path / f"{self.get_extended_boundary_box().get_format()}.osm"
|
||||
)
|
||||
get_osm(self.get_extended_boundary_box(), cache_file_path)
|
||||
|
||||
return OSMReader().parse_osm_file(cache_file_path)
|
||||
|
||||
def get_file_name(self, directory_name: Path) -> Path:
|
||||
"""
|
||||
|
@ -239,13 +232,13 @@ class Tile:
|
|||
:param cache_path: directory to store SVG and PNG tiles
|
||||
"""
|
||||
try:
|
||||
map_: Map = self.load_map(cache_path)
|
||||
osm_data: OSMData = self.load_osm_data(cache_path)
|
||||
except NetworkError as e:
|
||||
raise NetworkError(f"Map does not loaded. {e.message}")
|
||||
|
||||
self.draw_for_map(map_, directory_name)
|
||||
self.draw_for_map(osm_data, directory_name)
|
||||
|
||||
def draw_for_map(self, map_: Map, directory_name: Path) -> None:
|
||||
def draw_for_map(self, osm_data: OSMData, directory_name: Path) -> None:
|
||||
"""Draw tile using existing map."""
|
||||
lat1, lon1 = self.get_coordinates()
|
||||
lat2, lon2 = Tile(self.x + 1, self.y + 1, self.scale).get_coordinates()
|
||||
|
@ -266,17 +259,11 @@ class Tile:
|
|||
)
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
constructor: Constructor = Constructor(
|
||||
map_, flinger, scheme, icon_extractor
|
||||
osm_data, flinger, scheme, icon_extractor
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
painter: Painter = Painter(
|
||||
map_=map_,
|
||||
flinger=flinger,
|
||||
svg=svg,
|
||||
icon_extractor=icon_extractor,
|
||||
scheme=scheme,
|
||||
)
|
||||
painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
|
||||
painter.draw(constructor)
|
||||
|
||||
logging.info(f"Writing output SVG {output_file_name}...")
|
||||
|
@ -291,7 +278,7 @@ def ui(options) -> None:
|
|||
directory: Path = workspace.get_tile_path()
|
||||
|
||||
if options.coordinates:
|
||||
coordinates: List[float] = list(
|
||||
coordinates: list[float] = list(
|
||||
map(float, options.coordinates.strip().split(","))
|
||||
)
|
||||
tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale)
|
||||
|
|
107
roentgen/ui.py
107
roentgen/ui.py
|
@ -13,50 +13,48 @@ from dataclasses import dataclass
|
|||
|
||||
import numpy as np
|
||||
|
||||
from roentgen.osm_reader import STAGES_OF_DECAY
|
||||
|
||||
BOXES: str = " ▏▎▍▌▋▊▉"
|
||||
BOXES_LENGTH: int = len(BOXES)
|
||||
|
||||
AUTHOR_MODE: str = "author"
|
||||
TIME_MODE: str = "time"
|
||||
|
||||
LATITUDE_MAX_DIFFERENCE: float = 0.5
|
||||
LONGITUDE_MAX_DIFFERENCE: float = 0.5
|
||||
|
||||
|
||||
def parse_options(args) -> argparse.Namespace:
|
||||
"""
|
||||
Parse Röntgen command-line options.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
"""Parse Röntgen command-line options."""
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
||||
description="Röntgen. OpenStreetMap renderer with custom icon set"
|
||||
)
|
||||
subparser = parser.add_subparsers(dest="command")
|
||||
|
||||
render = subparser.add_parser("render")
|
||||
subparser.add_parser("icons")
|
||||
mapcss = subparser.add_parser("mapcss")
|
||||
subparser.add_parser("taginfo")
|
||||
tile = subparser.add_parser("tile")
|
||||
element = subparser.add_parser("element")
|
||||
server = subparser.add_parser("server")
|
||||
add_render_arguments(subparser.add_parser("render"))
|
||||
add_tile_arguments(subparser.add_parser("tile"))
|
||||
add_server_arguments(subparser.add_parser("server"))
|
||||
add_element_arguments(subparser.add_parser("element"))
|
||||
add_mapcss_arguments(subparser.add_parser("mapcss"))
|
||||
|
||||
add_render_arguments(render)
|
||||
add_tile_arguments(tile)
|
||||
add_server_arguments(server)
|
||||
add_element_arguments(element)
|
||||
add_mapcss_arguments(mapcss)
|
||||
subparser.add_parser("icons")
|
||||
subparser.add_parser("taginfo")
|
||||
|
||||
arguments: argparse.Namespace = parser.parse_args(args[1:])
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def add_tile_arguments(tile) -> None:
|
||||
def add_tile_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add arguments for tile command."""
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--coordinates",
|
||||
metavar="<latitude>,<longitude>",
|
||||
help="coordinates of any location inside the tile",
|
||||
)
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--scale",
|
||||
type=int,
|
||||
|
@ -64,19 +62,19 @@ def add_tile_arguments(tile) -> None:
|
|||
help="OSM zoom level",
|
||||
default=18,
|
||||
)
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--tile",
|
||||
metavar="<scale>/<x>/<y>",
|
||||
help="tile specification",
|
||||
)
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"--cache",
|
||||
help="path for temporary OSM files",
|
||||
default="cache",
|
||||
metavar="<path>",
|
||||
)
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--boundary-box",
|
||||
help="construct the minimum amount of tiles that cover requested "
|
||||
|
@ -85,9 +83,9 @@ def add_tile_arguments(tile) -> None:
|
|||
)
|
||||
|
||||
|
||||
def add_server_arguments(tile) -> None:
|
||||
def add_server_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add arguments for server command."""
|
||||
tile.add_argument(
|
||||
parser.add_argument(
|
||||
"--cache",
|
||||
help="path for temporary OSM files",
|
||||
default="cache",
|
||||
|
@ -95,16 +93,16 @@ def add_server_arguments(tile) -> None:
|
|||
)
|
||||
|
||||
|
||||
def add_element_arguments(element) -> None:
|
||||
def add_element_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add arguments for element command."""
|
||||
element.add_argument("-n", "--node")
|
||||
element.add_argument("-w", "--way")
|
||||
element.add_argument("-r", "--relation")
|
||||
parser.add_argument("-n", "--node")
|
||||
parser.add_argument("-w", "--way")
|
||||
parser.add_argument("-r", "--relation")
|
||||
|
||||
|
||||
def add_render_arguments(render) -> None:
|
||||
def add_render_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add arguments for render command."""
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input",
|
||||
dest="input_file_name",
|
||||
|
@ -113,7 +111,7 @@ def add_render_arguments(render) -> None:
|
|||
help="input XML file name or names (if not specified, file will be "
|
||||
"downloaded using OpenStreetMap API)",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
dest="output_file_name",
|
||||
|
@ -121,14 +119,14 @@ def add_render_arguments(render) -> None:
|
|||
default="out/map.svg",
|
||||
help="output SVG file name",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--boundary-box",
|
||||
metavar="<lon1>,<lat1>,<lon2>,<lat2>",
|
||||
help='geo boundary box, use space before "-" if the first value is '
|
||||
"negative",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--scale",
|
||||
metavar="<float>",
|
||||
|
@ -136,61 +134,63 @@ def add_render_arguments(render) -> None:
|
|||
default=18,
|
||||
type=float,
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--cache",
|
||||
help="path for temporary OSM files",
|
||||
default="cache",
|
||||
metavar="<path>",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--labels",
|
||||
help="label drawing mode: `no`, `main`, or `all`",
|
||||
dest="label_mode",
|
||||
default="main",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--overlap",
|
||||
dest="overlap",
|
||||
default=12,
|
||||
type=int,
|
||||
help="how many pixels should be left around icons and text",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
default="normal",
|
||||
help="map drawing mode",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
default="",
|
||||
help="seed for random",
|
||||
)
|
||||
render.add_argument(
|
||||
parser.add_argument(
|
||||
"--level",
|
||||
default=None,
|
||||
help="display only this floor level",
|
||||
)
|
||||
|
||||
|
||||
def add_mapcss_arguments(mapcss) -> None:
|
||||
def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add arguments for mapcss command."""
|
||||
mapcss.add_argument(
|
||||
parser.add_argument(
|
||||
"--icons",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="add icons for nodes and areas",
|
||||
)
|
||||
mapcss.add_argument(
|
||||
parser.add_argument(
|
||||
"--ways",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="add style for ways and relations",
|
||||
)
|
||||
mapcss.add_argument(
|
||||
parser.add_argument(
|
||||
"--lifecycle",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="add icons for lifecycle tags",
|
||||
help="add icons for lifecycle tags; be careful: this will increase the "
|
||||
f"number of node and area selectors by {len(STAGES_OF_DECAY) + 1} "
|
||||
f"times",
|
||||
)
|
||||
|
||||
|
||||
|
@ -245,6 +245,8 @@ class BoundaryBox:
|
|||
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2> or simply
|
||||
<left>,<bottom>,<right>,<top>.
|
||||
"""
|
||||
boundary_box = boundary_box.replace(" ", "")
|
||||
|
||||
matcher = re.match(
|
||||
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
|
||||
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
|
||||
|
@ -256,10 +258,10 @@ class BoundaryBox:
|
|||
return None
|
||||
|
||||
try:
|
||||
left = float(matcher.group("left"))
|
||||
bottom = float(matcher.group("bottom"))
|
||||
right = float(matcher.group("right"))
|
||||
top = float(matcher.group("top"))
|
||||
left: float = float(matcher.group("left"))
|
||||
bottom: float = float(matcher.group("bottom"))
|
||||
right: float = float(matcher.group("right"))
|
||||
top: float = float(matcher.group("top"))
|
||||
except ValueError:
|
||||
logging.fatal("Invalid boundary box.")
|
||||
return None
|
||||
|
@ -270,7 +272,10 @@ class BoundaryBox:
|
|||
if bottom >= top:
|
||||
logging.error("Negative vertical boundary.")
|
||||
return None
|
||||
if right - left > 0.5 or top - bottom > 0.5:
|
||||
if (
|
||||
right - left > LONGITUDE_MAX_DIFFERENCE
|
||||
or top - bottom > LATITUDE_MAX_DIFFERENCE
|
||||
):
|
||||
logging.error("Boundary box is too big.")
|
||||
return None
|
||||
|
||||
|
@ -285,9 +290,7 @@ class BoundaryBox:
|
|||
return self.bottom, self.right
|
||||
|
||||
def round(self) -> "BoundaryBox":
|
||||
"""
|
||||
Round boundary box.
|
||||
"""
|
||||
"""Round boundary box."""
|
||||
self.left = round(self.left * 1000) / 1000 - 0.001
|
||||
self.bottom = round(self.bottom * 1000) / 1000 - 0.001
|
||||
self.right = round(self.right * 1000) / 1000 + 0.001
|
||||
|
|
|
@ -18,28 +18,20 @@ class MinMax:
|
|||
max_: Any = None
|
||||
|
||||
def update(self, value: Any) -> None:
|
||||
"""
|
||||
Update minimum and maximum with new value.
|
||||
"""
|
||||
"""Update minimum and maximum with new value."""
|
||||
self.min_ = value if not self.min_ or value < self.min_ else self.min_
|
||||
self.max_ = value if not self.max_ or value > self.max_ else self.max_
|
||||
|
||||
def delta(self) -> Any:
|
||||
"""
|
||||
Difference between maximum and minimum.
|
||||
"""
|
||||
"""Difference between maximum and minimum."""
|
||||
return self.max_ - self.min_
|
||||
|
||||
def center(self) -> Any:
|
||||
"""
|
||||
Get middle point between minimum and maximum.
|
||||
"""
|
||||
"""Get middle point between minimum and maximum."""
|
||||
return (self.min_ + self.max_) / 2
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
Check if interval is empty.
|
||||
"""
|
||||
"""Check if interval is empty."""
|
||||
return self.min_ == self.max_
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
@ -12,12 +12,15 @@ def compute_angle(vector: np.array):
|
|||
For the given vector compute an angle between it and (1, 0) vector. The
|
||||
result is in [0, 2π].
|
||||
"""
|
||||
if vector[0] == 0:
|
||||
if vector[1] > 0:
|
||||
return np.pi / 2
|
||||
return np.pi + np.pi / 2
|
||||
if vector[0] < 0:
|
||||
return np.arctan(vector[1] / vector[0]) + np.pi
|
||||
if vector[1] < 0:
|
||||
return np.arctan(vector[1] / vector[0]) + 2 * np.pi
|
||||
else:
|
||||
return np.arctan(vector[1] / vector[0])
|
||||
return np.arctan(vector[1] / vector[0])
|
||||
|
||||
|
||||
def turn_by_angle(vector: np.array, angle: float):
|
||||
|
|
|
@ -15,6 +15,10 @@ def check_and_create(directory: Path) -> Path:
|
|||
|
||||
|
||||
class Workspace:
|
||||
"""
|
||||
Project file and directory paths and generated files and directories.
|
||||
"""
|
||||
|
||||
# Project directories and files, that are the part of the repository.
|
||||
|
||||
SCHEME_PATH: Path = Path("scheme")
|
||||
|
|
|
@ -8,9 +8,10 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_round_zero_coordinates() -> None:
|
||||
box: BoundaryBox = BoundaryBox(0, 0, 0, 0).round()
|
||||
|
||||
assert box.get_format() == "-0.001,-0.001,0.001,0.001"
|
||||
assert (
|
||||
BoundaryBox(0, 0, 0, 0).round().get_format()
|
||||
== "-0.001,-0.001,0.001,0.001"
|
||||
)
|
||||
|
||||
|
||||
def test_round_coordinates() -> None:
|
||||
|
|
|
@ -11,9 +11,7 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_is_bright() -> None:
|
||||
"""
|
||||
Test detecting color brightness.
|
||||
"""
|
||||
"""Test detecting color brightness."""
|
||||
assert is_bright(Color("white"))
|
||||
assert is_bright(Color("yellow"))
|
||||
assert not is_bright(Color("brown"))
|
||||
|
@ -21,9 +19,7 @@ def test_is_bright() -> None:
|
|||
|
||||
|
||||
def test_gradient() -> None:
|
||||
"""
|
||||
Test color picking from gradient.
|
||||
"""
|
||||
"""Test color picking from gradient."""
|
||||
color: Color = get_gradient_color(
|
||||
0.5, MinMax(0, 1), [Color("black"), Color("white")]
|
||||
)
|
||||
|
|
|
@ -10,9 +10,7 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_pseudo_mercator() -> None:
|
||||
"""
|
||||
Test pseudo-Mercator projection.
|
||||
"""
|
||||
"""Test pseudo-Mercator projection."""
|
||||
assert np.allclose(pseudo_mercator(np.array((0, 0))), np.array((0, 0)))
|
||||
assert np.allclose(pseudo_mercator(np.array((0, 10))), np.array((10, 0)))
|
||||
assert np.allclose(
|
||||
|
@ -21,9 +19,7 @@ def test_pseudo_mercator() -> None:
|
|||
|
||||
|
||||
def test_osm_zoom_level_to_pixels_per_meter() -> None:
|
||||
"""
|
||||
Test scale computation.
|
||||
"""
|
||||
"""Test scale computation."""
|
||||
assert np.allclose(
|
||||
osm_zoom_level_to_pixels_per_meter(18), 1.6759517949045808
|
||||
)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Test icon generation for nodes.
|
||||
"""
|
||||
from typing import Dict, Set
|
||||
|
||||
import pytest
|
||||
|
||||
from roentgen.grid import IconCollection
|
||||
|
@ -34,11 +32,9 @@ def test_icons_by_name(init_collection) -> None:
|
|||
init_collection.draw_icons(workspace.get_icons_by_name_path(), by_name=True)
|
||||
|
||||
|
||||
def get_icon(tags: Dict[str, str]) -> IconSet:
|
||||
"""
|
||||
Construct icon from tags.
|
||||
"""
|
||||
processed: Set[str] = set()
|
||||
def get_icon(tags: dict[str, str]) -> IconSet:
|
||||
"""Construct icon from tags."""
|
||||
processed: set[str] = set()
|
||||
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed)
|
||||
return icon
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""
|
||||
Test label generation for nodes.
|
||||
"""
|
||||
from typing import List, Set
|
||||
|
||||
from roentgen.text import Label
|
||||
from test import SCHEME
|
||||
|
||||
|
@ -10,18 +8,14 @@ __author__ = "Sergey Vartanov"
|
|||
__email__ = "me@enzet.ru"
|
||||
|
||||
|
||||
def construct_labels(tags) -> List[Label]:
|
||||
"""
|
||||
Construct labels from OSM node tags.
|
||||
"""
|
||||
processed: Set[str] = set()
|
||||
def construct_labels(tags) -> list[Label]:
|
||||
"""Construct labels from OSM node tags."""
|
||||
processed: set[str] = set()
|
||||
return SCHEME.construct_text(tags, "all", processed)
|
||||
|
||||
|
||||
def test_1_label() -> None:
|
||||
"""
|
||||
Test tags that should be converted into single label.
|
||||
"""
|
||||
"""Test tags that should be converted into single label."""
|
||||
labels = construct_labels({"name": "Name"})
|
||||
assert len(labels) == 1
|
||||
assert labels[0].text == "Name"
|
||||
|
@ -37,9 +31,7 @@ def test_1_label_unknown_tags() -> None:
|
|||
|
||||
|
||||
def test_2_labels() -> None:
|
||||
"""
|
||||
Test tags that should be converted into two labels.
|
||||
"""
|
||||
"""Test tags that should be converted into two labels."""
|
||||
labels = construct_labels({"name": "Name", "ref": "5"})
|
||||
assert len(labels) == 2
|
||||
assert labels[0].text == "Name"
|
||||
|
|
|
@ -10,9 +10,7 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_mapcss() -> None:
|
||||
"""
|
||||
Test MapCSS generation.
|
||||
"""
|
||||
"""Test MapCSS generation."""
|
||||
writer: MapCSSWriter = MapCSSWriter(SCHEME, "icons")
|
||||
matcher: NodeMatcher = NodeMatcher(
|
||||
{"tags": {"natural": "tree"}, "shapes": ["tree"]}
|
||||
|
|
|
@ -3,35 +3,31 @@ Test OSM XML parsing.
|
|||
"""
|
||||
import numpy as np
|
||||
|
||||
from roentgen.osm_reader import OSMNode, OSMReader, OSMRelation, OSMWay
|
||||
from roentgen.osm_reader import OSMNode, OSMReader, OSMRelation, OSMWay, OSMData
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
|
||||
def test_node() -> None:
|
||||
"""
|
||||
Test OSM node parsing from XML.
|
||||
"""
|
||||
reader = OSMReader()
|
||||
map_ = reader.parse_osm_text(
|
||||
"""Test OSM node parsing from XML."""
|
||||
reader: OSMReader = OSMReader()
|
||||
osm_data: OSMData = reader.parse_osm_text(
|
||||
"""<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="42" lon="5" lat="10" />
|
||||
</osm>"""
|
||||
)
|
||||
assert 42 in map_.nodes
|
||||
node: OSMNode = map_.nodes[42]
|
||||
assert 42 in osm_data.nodes
|
||||
node: OSMNode = osm_data.nodes[42]
|
||||
assert node.id_ == 42
|
||||
assert np.allclose(node.coordinates, np.array([10, 5]))
|
||||
|
||||
|
||||
def test_node_with_tag() -> None:
|
||||
"""
|
||||
Test OSM node parsing from XML.
|
||||
"""
|
||||
"""Test OSM node parsing from XML."""
|
||||
reader = OSMReader()
|
||||
map_ = reader.parse_osm_text(
|
||||
osm_data: OSMData = reader.parse_osm_text(
|
||||
"""<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="42" lon="5" lat="10">
|
||||
|
@ -39,35 +35,31 @@ def test_node_with_tag() -> None:
|
|||
</node>
|
||||
</osm>"""
|
||||
)
|
||||
assert 42 in map_.nodes
|
||||
node: OSMNode = map_.nodes[42]
|
||||
assert 42 in osm_data.nodes
|
||||
node: OSMNode = osm_data.nodes[42]
|
||||
assert node.id_ == 42
|
||||
assert np.allclose(node.coordinates, np.array([10, 5]))
|
||||
assert node.tags["key"] == "value"
|
||||
|
||||
|
||||
def test_way() -> None:
|
||||
"""
|
||||
Test OSM way parsing from XML.
|
||||
"""
|
||||
reader = OSMReader()
|
||||
map_ = reader.parse_osm_text(
|
||||
"""Test OSM way parsing from XML."""
|
||||
reader: OSMReader = OSMReader()
|
||||
osm_data: OSMData = reader.parse_osm_text(
|
||||
"""<?xml version="1.0"?>
|
||||
<osm>
|
||||
<way id="42" />
|
||||
</osm>"""
|
||||
)
|
||||
assert 42 in map_.ways
|
||||
way: OSMWay = map_.ways[42]
|
||||
assert 42 in osm_data.ways
|
||||
way: OSMWay = osm_data.ways[42]
|
||||
assert way.id_ == 42
|
||||
|
||||
|
||||
def test_nodes() -> None:
|
||||
"""
|
||||
Test OSM node parsing from XML.
|
||||
"""
|
||||
"""Test OSM node parsing from XML."""
|
||||
reader = OSMReader()
|
||||
map_ = reader.parse_osm_text(
|
||||
osm_data: OSMData = reader.parse_osm_text(
|
||||
"""<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="1" lon="5" lat="10" />
|
||||
|
@ -77,18 +69,16 @@ def test_nodes() -> None:
|
|||
</way>
|
||||
</osm>"""
|
||||
)
|
||||
way: OSMWay = map_.ways[2]
|
||||
way: OSMWay = osm_data.ways[2]
|
||||
assert len(way.nodes) == 1
|
||||
assert way.nodes[0].id_ == 1
|
||||
assert way.tags["key"] == "value"
|
||||
|
||||
|
||||
def test_relation() -> None:
|
||||
"""
|
||||
Test OSM node parsing from XML.
|
||||
"""
|
||||
reader = OSMReader()
|
||||
map_ = reader.parse_osm_text(
|
||||
"""Test OSM node parsing from XML."""
|
||||
reader: OSMReader = OSMReader()
|
||||
osm_data: OSMData = reader.parse_osm_text(
|
||||
"""<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="1" lon="5" lat="10" />
|
||||
|
@ -101,8 +91,8 @@ def test_relation() -> None:
|
|||
</relation>
|
||||
</osm>"""
|
||||
)
|
||||
assert 3 in map_.relations
|
||||
relation: OSMRelation = map_.relations[3]
|
||||
assert 3 in osm_data.relations
|
||||
relation: OSMRelation = osm_data.relations[3]
|
||||
assert relation.id_ == 3
|
||||
assert relation.tags["key"] == "value"
|
||||
assert len(relation.members) == 1
|
||||
|
|
|
@ -8,23 +8,17 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_style_empty() -> None:
|
||||
"""
|
||||
Test constructing style of empty tags.
|
||||
"""
|
||||
"""Test constructing style of empty tags."""
|
||||
assert SCHEME.get_style({}, 18) == []
|
||||
|
||||
|
||||
def test_style_unknown() -> None:
|
||||
"""
|
||||
Test constructing style of unknown tags.
|
||||
"""
|
||||
"""Test constructing style of unknown tags."""
|
||||
assert SCHEME.get_style({"aaa": "bbb"}, 18) == []
|
||||
|
||||
|
||||
def test_style_area() -> None:
|
||||
"""
|
||||
Test constructing style of landuse=grass.
|
||||
"""
|
||||
"""Test constructing style of landuse=grass."""
|
||||
style = SCHEME.get_style({"landuse": "grass"}, 18)
|
||||
assert len(style) == 1
|
||||
assert style[0].style == {"fill": "#CFE0A8", "stroke": "#BFD098"}
|
||||
|
|
|
@ -10,18 +10,14 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def check_length(value: str, expected: Optional[float]) -> None:
|
||||
"""
|
||||
Assert that constructed value is equals to an expected one.
|
||||
"""
|
||||
"""Assert that constructed value is equals to an expected one."""
|
||||
tagged = Tagged()
|
||||
tagged.tags["a"] = value
|
||||
assert tagged.get_length("a") == expected
|
||||
|
||||
|
||||
def test_meters() -> None:
|
||||
"""
|
||||
Test length in meters processing.
|
||||
"""
|
||||
"""Test length in meters processing."""
|
||||
check_length("50m", 50.0)
|
||||
check_length("50.m", 50.0)
|
||||
check_length("50.05m", 50.05)
|
||||
|
@ -31,16 +27,12 @@ def test_meters() -> None:
|
|||
|
||||
|
||||
def test_kilometers() -> None:
|
||||
"""
|
||||
Test length in meters processing.
|
||||
"""
|
||||
"""Test length in meters processing."""
|
||||
check_length("50km", 50_000.0)
|
||||
check_length("50 km", 50_000.0)
|
||||
|
||||
|
||||
def test_miles() -> None:
|
||||
"""
|
||||
Test length in meters processing.
|
||||
"""
|
||||
"""Test length in meters processing."""
|
||||
check_length("1mi", 1609.344)
|
||||
check_length("50 mi", 50 * 1609.344)
|
||||
|
|
|
@ -8,8 +8,6 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
|
||||
def test_voltage() -> None:
|
||||
"""
|
||||
Test voltage tag value processing.
|
||||
"""
|
||||
"""Test voltage tag value processing."""
|
||||
assert format_voltage("42") == "42 V"
|
||||
assert format_voltage("42000") == "42 kV"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue