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