mirror of
https://github.com/enzet/map-machine.git
synced 2025-08-06 10:09:52 +02:00
Refactor entry point.
This commit is contained in:
parent
cbfdb4de78
commit
9555a5d74f
9 changed files with 400 additions and 266 deletions
246
roentgen.py
246
roentgen.py
|
@ -1,250 +1,8 @@
|
||||||
"""
|
"""
|
||||||
Röntgen entry point.
|
Röntgen entry point.
|
||||||
|
|
||||||
Author: Sergey Vartanov (me@enzet.ru).
|
|
||||||
"""
|
"""
|
||||||
import argparse
|
from roentgen.main import main
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Set
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import svgwrite
|
|
||||||
|
|
||||||
from roentgen.constructor import Constructor
|
|
||||||
from roentgen.flinger import Flinger
|
|
||||||
from roentgen.grid import draw_icons
|
|
||||||
from roentgen.icon import ShapeExtractor
|
|
||||||
from roentgen.mapper import (
|
|
||||||
AUTHOR_MODE,
|
|
||||||
TIME_MODE,
|
|
||||||
Map,
|
|
||||||
check_level_number,
|
|
||||||
check_level_overground,
|
|
||||||
)
|
|
||||||
from roentgen.osm_getter import NetworkError, get_osm
|
|
||||||
from roentgen.osm_reader import OSMData, OSMReader, OverpassReader
|
|
||||||
from roentgen.point import Point
|
|
||||||
from roentgen.scheme import LineStyle, Scheme
|
|
||||||
from roentgen.ui import BoundaryBox, parse_options
|
|
||||||
from roentgen.util import MinMax
|
|
||||||
from roentgen.workspace import Workspace
|
|
||||||
|
|
||||||
|
|
||||||
def main(options) -> None:
|
|
||||||
"""
|
|
||||||
Röntgen entry point.
|
|
||||||
|
|
||||||
: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:
|
|
||||||
boundary_box: BoundaryBox = BoundaryBox.from_text(options.boundary_box)
|
|
||||||
|
|
||||||
cache_path: Path = Path(options.cache)
|
|
||||||
cache_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
input_file_names: List[Path]
|
|
||||||
|
|
||||||
if options.input_file_name:
|
|
||||||
input_file_names = list(map(Path, options.input_file_name))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
cache_file_path: Path = (
|
|
||||||
cache_path / f"{boundary_box.get_format()}.osm"
|
|
||||||
)
|
|
||||||
get_osm(boundary_box, cache_file_path)
|
|
||||||
except NetworkError as e:
|
|
||||||
logging.fatal(e.message)
|
|
||||||
sys.exit(1)
|
|
||||||
input_file_names = [cache_file_path]
|
|
||||||
|
|
||||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
|
||||||
min_: np.array
|
|
||||||
max_: np.array
|
|
||||||
osm_data: OSMData
|
|
||||||
|
|
||||||
if input_file_names[0].name.endswith(".json"):
|
|
||||||
reader: OverpassReader = OverpassReader()
|
|
||||||
reader.parse_json_file(input_file_names[0])
|
|
||||||
|
|
||||||
osm_data = reader.osm_data
|
|
||||||
view_box = MinMax(
|
|
||||||
np.array(
|
|
||||||
(osm_data.boundary_box[0].min_, osm_data.boundary_box[1].min_)
|
|
||||||
),
|
|
||||||
np.array(
|
|
||||||
(osm_data.boundary_box[0].max_, osm_data.boundary_box[1].max_)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE]
|
|
||||||
osm_reader = OSMReader(is_full=is_full)
|
|
||||||
|
|
||||||
for file_name in input_file_names:
|
|
||||||
if not file_name.is_file():
|
|
||||||
print(f"Fatal: no such file: {file_name}.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
osm_reader.parse_osm_file(file_name)
|
|
||||||
|
|
||||||
osm_data = osm_reader.osm_data
|
|
||||||
|
|
||||||
if options.boundary_box:
|
|
||||||
view_box = MinMax(
|
|
||||||
np.array((boundary_box.bottom, boundary_box.left)),
|
|
||||||
np.array((boundary_box.top, boundary_box.right)),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
view_box = osm_data.view_box
|
|
||||||
|
|
||||||
flinger: Flinger = Flinger(view_box, options.scale)
|
|
||||||
size: np.array = flinger.size
|
|
||||||
|
|
||||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
|
||||||
options.output_file_name, size=size
|
|
||||||
)
|
|
||||||
icon_extractor: ShapeExtractor = ShapeExtractor(
|
|
||||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
|
||||||
)
|
|
||||||
|
|
||||||
if options.level:
|
|
||||||
if options.level == "overground":
|
|
||||||
check_level = check_level_overground
|
|
||||||
elif options.level == "underground":
|
|
||||||
|
|
||||||
def check_level(x) -> bool:
|
|
||||||
"""Draw underground objects."""
|
|
||||||
return not check_level_overground(x)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def check_level(x) -> bool:
|
|
||||||
"""Draw objects on the specified level."""
|
|
||||||
return not check_level_number(x, float(options.level))
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
def check_level(_) -> bool:
|
|
||||||
"""Draw objects on any level."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
constructor: Constructor = Constructor(
|
|
||||||
osm_data,
|
|
||||||
flinger,
|
|
||||||
scheme,
|
|
||||||
icon_extractor,
|
|
||||||
check_level,
|
|
||||||
options.mode,
|
|
||||||
options.seed,
|
|
||||||
)
|
|
||||||
constructor.construct()
|
|
||||||
|
|
||||||
painter: Map = Map(
|
|
||||||
overlap=options.overlap,
|
|
||||||
mode=options.mode,
|
|
||||||
label_mode=options.label_mode,
|
|
||||||
flinger=flinger,
|
|
||||||
svg=svg,
|
|
||||||
scheme=scheme,
|
|
||||||
)
|
|
||||||
painter.draw(constructor)
|
|
||||||
|
|
||||||
print(f"Writing output SVG to {options.output_file_name}...")
|
|
||||||
with open(options.output_file_name, "w") as output_file:
|
|
||||||
svg.write(output_file)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_element(options) -> None:
|
|
||||||
"""Draw single node, line, or area."""
|
|
||||||
if options.node:
|
|
||||||
target: str = "node"
|
|
||||||
tags_description = options.node
|
|
||||||
else:
|
|
||||||
# Not implemented yet.
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
tags: dict[str, str] = dict(
|
|
||||||
[x.split("=") for x in tags_description.split(",")]
|
|
||||||
)
|
|
||||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
|
||||||
extractor: ShapeExtractor = ShapeExtractor(
|
|
||||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
|
||||||
)
|
|
||||||
processed: Set[str] = set()
|
|
||||||
icon, priority = scheme.get_icon(extractor, tags, processed)
|
|
||||||
is_for_node: bool = target == "node"
|
|
||||||
labels = scheme.construct_text(tags, "all", processed)
|
|
||||||
point = Point(
|
|
||||||
icon,
|
|
||||||
labels,
|
|
||||||
tags,
|
|
||||||
processed,
|
|
||||||
np.array((32, 32)),
|
|
||||||
None,
|
|
||||||
is_for_node=is_for_node,
|
|
||||||
draw_outline=is_for_node,
|
|
||||||
)
|
|
||||||
border: np.array = np.array((16, 16))
|
|
||||||
size: np.array = point.get_size() + border
|
|
||||||
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
|
|
||||||
|
|
||||||
output_file_path: Path = workspace.output_path / "element.svg"
|
|
||||||
svg = svgwrite.Drawing(str(output_file_path), size.astype(float))
|
|
||||||
for style in scheme.get_style(tags):
|
|
||||||
style: LineStyle
|
|
||||||
path = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
|
|
||||||
path.update(style.style)
|
|
||||||
svg.add(path)
|
|
||||||
point.draw_main_shapes(svg)
|
|
||||||
point.draw_extra_shapes(svg)
|
|
||||||
point.draw_texts(svg)
|
|
||||||
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:
|
|
||||||
"""Initialize default scheme."""
|
|
||||||
return Scheme(workspace.DEFAULT_SCHEME_PATH)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
|
|
||||||
workspace: Workspace = Workspace(Path("out"))
|
|
||||||
|
|
||||||
arguments: argparse.Namespace = parse_options(sys.argv)
|
|
||||||
|
|
||||||
if arguments.command == "render":
|
|
||||||
main(arguments)
|
|
||||||
|
|
||||||
elif arguments.command == "tile":
|
|
||||||
from roentgen import tile
|
|
||||||
|
|
||||||
tile.ui(arguments)
|
|
||||||
|
|
||||||
elif arguments.command == "icons":
|
|
||||||
draw_icons()
|
|
||||||
|
|
||||||
elif arguments.command == "mapcss":
|
|
||||||
from roentgen import mapcss
|
|
||||||
|
|
||||||
mapcss.ui(arguments)
|
|
||||||
|
|
||||||
elif arguments.command == "element":
|
|
||||||
draw_element(arguments)
|
|
||||||
|
|
||||||
elif arguments.command == "server":
|
|
||||||
from roentgen import server
|
|
||||||
|
|
||||||
server.ui(arguments)
|
|
||||||
|
|
||||||
elif arguments.command == "taginfo":
|
|
||||||
from roentgen.taginfo import write_taginfo_project_file
|
|
||||||
|
|
||||||
write_taginfo_project_file(init_scheme())
|
|
||||||
|
|
111
roentgen/boundary_box.py
Normal file
111
roentgen/boundary_box.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
LATITUDE_MAX_DIFFERENCE: float = 0.5
|
||||||
|
LONGITUDE_MAX_DIFFERENCE: float = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BoundaryBox:
|
||||||
|
"""
|
||||||
|
Rectangle that limit space on the map.
|
||||||
|
"""
|
||||||
|
|
||||||
|
left: float # Minimum longitude.
|
||||||
|
bottom: float # Minimum latitude.
|
||||||
|
right: float # Maximum longitude.
|
||||||
|
top: float # Maximum latitude.
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_text(cls, boundary_box: str):
|
||||||
|
"""
|
||||||
|
Parse boundary box string representation.
|
||||||
|
|
||||||
|
Note, that:
|
||||||
|
left < right
|
||||||
|
bottom < top
|
||||||
|
|
||||||
|
:param boundary_box: boundary box string representation in the form of
|
||||||
|
<minimum longitude>,<minimum latitude>,
|
||||||
|
<maximum longitude>,<maximum latitude> or simply
|
||||||
|
<left>,<bottom>,<right>,<top>.
|
||||||
|
"""
|
||||||
|
boundary_box = boundary_box.replace(" ", "")
|
||||||
|
|
||||||
|
matcher = re.match(
|
||||||
|
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
|
||||||
|
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
|
||||||
|
boundary_box,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matcher:
|
||||||
|
logging.fatal("Invalid boundary box.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
left: float = float(matcher.group("left"))
|
||||||
|
bottom: float = float(matcher.group("bottom"))
|
||||||
|
right: float = float(matcher.group("right"))
|
||||||
|
top: float = float(matcher.group("top"))
|
||||||
|
except ValueError:
|
||||||
|
logging.fatal("Invalid boundary box.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if left >= right:
|
||||||
|
logging.fatal("Negative horizontal boundary.")
|
||||||
|
return None
|
||||||
|
if bottom >= top:
|
||||||
|
logging.error("Negative vertical boundary.")
|
||||||
|
return None
|
||||||
|
if (
|
||||||
|
right - left > LONGITUDE_MAX_DIFFERENCE
|
||||||
|
or top - bottom > LATITUDE_MAX_DIFFERENCE
|
||||||
|
):
|
||||||
|
logging.error("Boundary box is too big.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cls(left, bottom, right, top)
|
||||||
|
|
||||||
|
def min_(self) -> np.ndarray:
|
||||||
|
"""Get minimum coordinates."""
|
||||||
|
return np.array((self.bottom, self.left))
|
||||||
|
|
||||||
|
def max_(self) -> np.ndarray:
|
||||||
|
"""Get maximum coordinates."""
|
||||||
|
return np.array((self.top, self.right))
|
||||||
|
|
||||||
|
def get_left_top(self) -> (np.ndarray, np.ndarray):
|
||||||
|
"""Get left top corner of the boundary box."""
|
||||||
|
return self.top, self.left
|
||||||
|
|
||||||
|
def get_right_bottom(self) -> (np.ndarray, np.ndarray):
|
||||||
|
"""Get right bottom corner of the boundary box."""
|
||||||
|
return self.bottom, self.right
|
||||||
|
|
||||||
|
def round(self) -> "BoundaryBox":
|
||||||
|
"""Round boundary box."""
|
||||||
|
self.left = round(self.left * 1000) / 1000 - 0.001
|
||||||
|
self.bottom = round(self.bottom * 1000) / 1000 - 0.001
|
||||||
|
self.right = round(self.right * 1000) / 1000 + 0.001
|
||||||
|
self.top = round(self.top * 1000) / 1000 + 0.001
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def center(self) -> np.ndarray:
|
||||||
|
"""Return center point of boundary box."""
|
||||||
|
return np.array(
|
||||||
|
((self.left + self.right) / 2, (self.top + self.bottom) / 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_format(self) -> str:
|
||||||
|
"""
|
||||||
|
Get text representation of the boundary box:
|
||||||
|
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are
|
||||||
|
rounded to three digits after comma.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f"{self.left:.3f},{self.bottom:.3f},{self.right:.3f},{self.top:.3f}"
|
||||||
|
)
|
60
roentgen/element.py
Normal file
60
roentgen/element.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import svgwrite
|
||||||
|
|
||||||
|
from roentgen.workspace import workspace
|
||||||
|
from roentgen.icon import ShapeExtractor
|
||||||
|
from roentgen.point import Point
|
||||||
|
from roentgen.scheme import LineStyle, Scheme
|
||||||
|
|
||||||
|
|
||||||
|
def draw_element(options) -> None:
|
||||||
|
"""Draw single node, line, or area."""
|
||||||
|
if options.node:
|
||||||
|
target: str = "node"
|
||||||
|
tags_description = options.node
|
||||||
|
else:
|
||||||
|
# Not implemented yet.
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
tags: dict[str, str] = dict(
|
||||||
|
[x.split("=") for x in tags_description.split(",")]
|
||||||
|
)
|
||||||
|
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||||
|
extractor: ShapeExtractor = ShapeExtractor(
|
||||||
|
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||||
|
)
|
||||||
|
processed: set[str] = set()
|
||||||
|
icon, priority = scheme.get_icon(extractor, tags, processed)
|
||||||
|
is_for_node: bool = target == "node"
|
||||||
|
labels = scheme.construct_text(tags, "all", processed)
|
||||||
|
point = Point(
|
||||||
|
icon,
|
||||||
|
labels,
|
||||||
|
tags,
|
||||||
|
processed,
|
||||||
|
np.array((32, 32)),
|
||||||
|
None,
|
||||||
|
is_for_node=is_for_node,
|
||||||
|
draw_outline=is_for_node,
|
||||||
|
)
|
||||||
|
border: np.array = np.array((16, 16))
|
||||||
|
size: np.array = point.get_size() + border
|
||||||
|
point.point = np.array((size[0] / 2, 16 / 2 + border[1] / 2))
|
||||||
|
|
||||||
|
output_file_path: Path = workspace.output_path / "element.svg"
|
||||||
|
svg = svgwrite.Drawing(str(output_file_path), size.astype(float))
|
||||||
|
for style in scheme.get_style(tags):
|
||||||
|
style: LineStyle
|
||||||
|
path = svg.path(d="M 0,0 L 64,0 L 64,64 L 0,64 L 0,0 Z")
|
||||||
|
path.update(style.style)
|
||||||
|
svg.add(path)
|
||||||
|
point.draw_main_shapes(svg)
|
||||||
|
point.draw_extra_shapes(svg)
|
||||||
|
point.draw_texts(svg)
|
||||||
|
with output_file_path.open("w+") as output_file:
|
||||||
|
svg.write(output_file)
|
||||||
|
logging.info(f"Element is written to {output_file_path}.")
|
|
@ -121,6 +121,7 @@ class IconCollection:
|
||||||
by_name: bool = False,
|
by_name: bool = False,
|
||||||
color: Optional[Color] = None,
|
color: Optional[Color] = None,
|
||||||
outline: bool = False,
|
outline: bool = False,
|
||||||
|
outline_opacity: float = 1.0,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param output_directory: path to the directory to store individual SVG
|
:param output_directory: path to the directory to store individual SVG
|
||||||
|
@ -128,6 +129,7 @@ class IconCollection:
|
||||||
:param by_name: use names instead of identifiers
|
:param by_name: use names instead of identifiers
|
||||||
:param color: fill color
|
:param color: fill color
|
||||||
:param outline: if true, draw outline beneath the icon
|
:param outline: if true, draw outline beneath the icon
|
||||||
|
:param outline_opacity: opacity of the outline
|
||||||
"""
|
"""
|
||||||
if by_name:
|
if by_name:
|
||||||
|
|
||||||
|
@ -146,6 +148,7 @@ class IconCollection:
|
||||||
output_directory / get_file_name(icon),
|
output_directory / get_file_name(icon),
|
||||||
color=color,
|
color=color,
|
||||||
outline=outline,
|
outline=outline,
|
||||||
|
outline_opacity=outline_opacity,
|
||||||
)
|
)
|
||||||
|
|
||||||
def draw_grid(
|
def draw_grid(
|
||||||
|
|
|
@ -330,6 +330,7 @@ class ShapeSpecification:
|
||||||
point: np.array,
|
point: np.array,
|
||||||
tags: dict[str, Any] = None,
|
tags: dict[str, Any] = None,
|
||||||
outline: bool = False,
|
outline: bool = False,
|
||||||
|
outline_opacity: float = 1.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Draw icon shape into SVG file.
|
Draw icon shape into SVG file.
|
||||||
|
@ -338,6 +339,7 @@ class ShapeSpecification:
|
||||||
:param point: 2D position of the shape centre
|
:param point: 2D position of the shape centre
|
||||||
:param tags: tags to be displayed as hint
|
:param tags: tags to be displayed as hint
|
||||||
:param outline: draw outline for the shape
|
:param outline: draw outline for the shape
|
||||||
|
:param outline_opacity: opacity of the outline
|
||||||
"""
|
"""
|
||||||
scale: np.array = np.array((1, 1))
|
scale: np.array = np.array((1, 1))
|
||||||
if self.flip_vertically:
|
if self.flip_vertically:
|
||||||
|
@ -358,6 +360,7 @@ class ShapeSpecification:
|
||||||
"stroke": color.hex,
|
"stroke": color.hex,
|
||||||
"stroke-width": 2.2,
|
"stroke-width": 2.2,
|
||||||
"stroke-linejoin": "round",
|
"stroke-linejoin": "round",
|
||||||
|
"opacity": outline_opacity,
|
||||||
}
|
}
|
||||||
path.update(style)
|
path.update(style)
|
||||||
if tags:
|
if tags:
|
||||||
|
@ -431,6 +434,7 @@ class Icon:
|
||||||
file_name: Path,
|
file_name: Path,
|
||||||
color: Optional[Color] = None,
|
color: Optional[Color] = None,
|
||||||
outline: bool = False,
|
outline: bool = False,
|
||||||
|
outline_opacity: float = 1.0,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Draw icon to the SVG file.
|
Draw icon to the SVG file.
|
||||||
|
@ -438,13 +442,16 @@ class Icon:
|
||||||
:param file_name: output SVG file name
|
:param file_name: output SVG file name
|
||||||
:param color: fill color
|
:param color: fill color
|
||||||
:param outline: if true, draw outline beneath the icon
|
:param outline: if true, draw outline beneath the icon
|
||||||
|
:param outline_opacity: opacity of the outline
|
||||||
"""
|
"""
|
||||||
svg: Drawing = Drawing(str(file_name), (16, 16))
|
svg: Drawing = Drawing(str(file_name), (16, 16))
|
||||||
|
|
||||||
for shape_specification in self.shape_specifications:
|
for shape_specification in self.shape_specifications:
|
||||||
if color:
|
if color:
|
||||||
shape_specification.color = color
|
shape_specification.color = color
|
||||||
shape_specification.draw(svg, (8, 8), outline=outline)
|
shape_specification.draw(
|
||||||
|
svg, (8, 8), outline=outline, outline_opacity=outline_opacity
|
||||||
|
)
|
||||||
|
|
||||||
for shape_specification in self.shape_specifications:
|
for shape_specification in self.shape_specifications:
|
||||||
if color:
|
if color:
|
||||||
|
|
63
roentgen/main.py
Normal file
63
roentgen/main.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
"""
|
||||||
|
Röntgen entry point.
|
||||||
|
|
||||||
|
Author: Sergey Vartanov (me@enzet.ru).
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from roentgen.element import draw_element
|
||||||
|
from roentgen.grid import draw_icons
|
||||||
|
from roentgen.scheme import Scheme
|
||||||
|
from roentgen.ui import parse_options
|
||||||
|
from roentgen.workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
|
def init_scheme(workspace: Workspace) -> Scheme:
|
||||||
|
"""Initialize default scheme."""
|
||||||
|
return Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Röntgen command-line entry point."""
|
||||||
|
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
|
||||||
|
workspace: Workspace = Workspace(Path("out"))
|
||||||
|
|
||||||
|
arguments: argparse.Namespace = parse_options(sys.argv)
|
||||||
|
|
||||||
|
if arguments.command == "render":
|
||||||
|
from roentgen import mapper
|
||||||
|
|
||||||
|
mapper.ui(arguments)
|
||||||
|
|
||||||
|
elif arguments.command == "tile":
|
||||||
|
from roentgen import tile
|
||||||
|
|
||||||
|
tile.ui(arguments)
|
||||||
|
|
||||||
|
elif arguments.command == "icons":
|
||||||
|
draw_icons()
|
||||||
|
|
||||||
|
elif arguments.command == "mapcss":
|
||||||
|
from roentgen import mapcss
|
||||||
|
|
||||||
|
mapcss.ui(arguments)
|
||||||
|
|
||||||
|
elif arguments.command == "element":
|
||||||
|
draw_element(arguments)
|
||||||
|
|
||||||
|
elif arguments.command == "server":
|
||||||
|
from roentgen import server
|
||||||
|
|
||||||
|
server.ui(arguments)
|
||||||
|
|
||||||
|
elif arguments.command == "taginfo":
|
||||||
|
from roentgen.taginfo import write_taginfo_project_file
|
||||||
|
|
||||||
|
write_taginfo_project_file(init_scheme(workspace))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -100,7 +100,7 @@ class MapCSSWriter:
|
||||||
:param matcher: tag matcher of Röntgen scheme
|
:param matcher: tag matcher of Röntgen scheme
|
||||||
:param prefix: tag prefix
|
:param prefix: tag prefix
|
||||||
:param opacity: icon opacity
|
:param opacity: icon opacity
|
||||||
:return:
|
:return: string representation of selector
|
||||||
"""
|
"""
|
||||||
elements: dict[str, str] = {}
|
elements: dict[str, str] = {}
|
||||||
|
|
||||||
|
@ -190,7 +190,10 @@ def ui(options) -> None:
|
||||||
)
|
)
|
||||||
collection: IconCollection = IconCollection.from_scheme(scheme, extractor)
|
collection: IconCollection = IconCollection.from_scheme(scheme, extractor)
|
||||||
collection.draw_icons(
|
collection.draw_icons(
|
||||||
icons_with_outline_path, color=Color("black"), outline=True
|
icons_with_outline_path,
|
||||||
|
color=Color("black"),
|
||||||
|
outline=True,
|
||||||
|
outline_opacity=0.5,
|
||||||
)
|
)
|
||||||
mapcss_writer: MapCSSWriter = MapCSSWriter(
|
mapcss_writer: MapCSSWriter = MapCSSWriter(
|
||||||
scheme,
|
scheme,
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
"""
|
"""
|
||||||
Simple OpenStreetMap renderer.
|
Simple OpenStreetMap renderer.
|
||||||
"""
|
"""
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Iterator
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import svgwrite
|
import svgwrite
|
||||||
from colour import Color
|
from colour import Color
|
||||||
from svgwrite.container import Group
|
from svgwrite.container import Group
|
||||||
from svgwrite.path import Path
|
from svgwrite.path import Path as SVGPath
|
||||||
from svgwrite.shapes import Rect
|
from svgwrite.shapes import Rect
|
||||||
|
|
||||||
from roentgen import ui
|
from roentgen.icon import ShapeExtractor
|
||||||
|
from roentgen.osm_getter import NetworkError, get_osm
|
||||||
from roentgen.constructor import Constructor
|
from roentgen.constructor import Constructor
|
||||||
from roentgen.figure import Road
|
from roentgen.figure import Road
|
||||||
from roentgen.flinger import Flinger
|
from roentgen.flinger import Flinger
|
||||||
from roentgen.osm_reader import OSMNode
|
from roentgen.osm_reader import OSMData, OSMNode, OSMReader, OverpassReader
|
||||||
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
|
from roentgen.ui import AUTHOR_MODE, BoundaryBox, TIME_MODE, progress_bar
|
||||||
|
from roentgen.util import MinMax
|
||||||
|
from roentgen.workspace import workspace
|
||||||
|
|
||||||
__author__ = "Sergey Vartanov"
|
__author__ = "Sergey Vartanov"
|
||||||
__email__ = "me@enzet.ru"
|
__email__ = "me@enzet.ru"
|
||||||
|
@ -58,13 +63,13 @@ class Map:
|
||||||
ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
|
ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
|
||||||
ways_length: int = len(ways)
|
ways_length: int = len(ways)
|
||||||
for index, way in enumerate(ways):
|
for index, way in enumerate(ways):
|
||||||
ui.progress_bar(index, ways_length, step=10, text="Drawing ways")
|
progress_bar(index, ways_length, step=10, text="Drawing ways")
|
||||||
path_commands: str = way.get_path(self.flinger)
|
path_commands: str = way.get_path(self.flinger)
|
||||||
if path_commands:
|
if path_commands:
|
||||||
path = Path(d=path_commands)
|
path = SVGPath(d=path_commands)
|
||||||
path.update(way.line_style.style)
|
path.update(way.line_style.style)
|
||||||
self.svg.add(path)
|
self.svg.add(path)
|
||||||
ui.progress_bar(-1, 0, text="Drawing ways")
|
progress_bar(-1, 0, text="Drawing ways")
|
||||||
|
|
||||||
roads: Iterator[Road] = sorted(
|
roads: Iterator[Road] = sorted(
|
||||||
constructor.roads, key=lambda x: x.matcher.priority
|
constructor.roads, key=lambda x: x.matcher.priority
|
||||||
|
@ -95,19 +100,17 @@ class Map:
|
||||||
steps: int = len(nodes)
|
steps: int = len(nodes)
|
||||||
|
|
||||||
for index, node in enumerate(nodes):
|
for index, node in enumerate(nodes):
|
||||||
ui.progress_bar(
|
progress_bar(index, steps * 3, step=10, text="Drawing main icons")
|
||||||
index, steps * 3, step=10, text="Drawing main icons"
|
|
||||||
)
|
|
||||||
node.draw_main_shapes(self.svg, occupied)
|
node.draw_main_shapes(self.svg, occupied)
|
||||||
|
|
||||||
for index, point in enumerate(nodes):
|
for index, point in enumerate(nodes):
|
||||||
ui.progress_bar(
|
progress_bar(
|
||||||
steps + index, steps * 3, step=10, text="Drawing extra icons"
|
steps + index, steps * 3, step=10, text="Drawing extra icons"
|
||||||
)
|
)
|
||||||
point.draw_extra_shapes(self.svg, occupied)
|
point.draw_extra_shapes(self.svg, occupied)
|
||||||
|
|
||||||
for index, point in enumerate(nodes):
|
for index, point in enumerate(nodes):
|
||||||
ui.progress_bar(
|
progress_bar(
|
||||||
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
|
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
|
@ -116,7 +119,7 @@ class Map:
|
||||||
):
|
):
|
||||||
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")
|
progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
|
||||||
|
|
||||||
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."""
|
||||||
|
@ -129,7 +132,7 @@ class Map:
|
||||||
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")
|
progress_bar(index, count, step=1, text="Drawing buildings")
|
||||||
fill: Color()
|
fill: Color()
|
||||||
for building in constructor.buildings:
|
for building in constructor.buildings:
|
||||||
if building.height < height or building.min_height > height:
|
if building.height < height or building.min_height > height:
|
||||||
|
@ -142,7 +145,7 @@ class Map:
|
||||||
|
|
||||||
previous_height = height
|
previous_height = height
|
||||||
|
|
||||||
ui.progress_bar(-1, count, step=1, text="Drawing buildings")
|
progress_bar(-1, count, step=1, text="Drawing buildings")
|
||||||
|
|
||||||
def draw_road(
|
def draw_road(
|
||||||
self, road: Road, color: Color, extra_width: float = 0
|
self, road: Road, color: Color, extra_width: float = 0
|
||||||
|
@ -157,7 +160,7 @@ class Map:
|
||||||
width = road.matcher.default_width
|
width = road.matcher.default_width
|
||||||
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 = SVGPath(d=path_commands)
|
||||||
style: dict[str, Any] = {
|
style: dict[str, Any] = {
|
||||||
"fill": "none",
|
"fill": "none",
|
||||||
"stroke": color.hex,
|
"stroke": color.hex,
|
||||||
|
@ -196,9 +199,8 @@ class Map:
|
||||||
parts = nodes[node]
|
parts = nodes[node]
|
||||||
if len(parts) < 4:
|
if len(parts) < 4:
|
||||||
continue
|
continue
|
||||||
scale: float = self.flinger.get_scale(node.coordinates)
|
|
||||||
intersection: Intersection = Intersection(list(parts))
|
intersection: Intersection = Intersection(list(parts))
|
||||||
intersection.draw(self.svg, scale, True)
|
intersection.draw(self.svg, True)
|
||||||
|
|
||||||
|
|
||||||
def check_level_number(tags: dict[str, Any], level: float):
|
def check_level_number(tags: dict[str, Any], level: float):
|
||||||
|
@ -237,3 +239,130 @@ def check_level_overground(tags: dict[str, Any]) -> bool:
|
||||||
if "parking" in tags and tags["parking"] == "underground":
|
if "parking" in tags and tags["parking"] == "underground":
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ui(options) -> None:
|
||||||
|
"""
|
||||||
|
Röntgen entry point.
|
||||||
|
|
||||||
|
: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:
|
||||||
|
boundary_box: BoundaryBox = BoundaryBox.from_text(options.boundary_box)
|
||||||
|
|
||||||
|
cache_path: Path = Path(options.cache)
|
||||||
|
cache_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
input_file_names: list[Path]
|
||||||
|
|
||||||
|
if options.input_file_name:
|
||||||
|
input_file_names = list(map(Path, options.input_file_name))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cache_file_path: Path = (
|
||||||
|
cache_path / f"{boundary_box.get_format()}.osm"
|
||||||
|
)
|
||||||
|
get_osm(boundary_box, cache_file_path)
|
||||||
|
except NetworkError as e:
|
||||||
|
logging.fatal(e.message)
|
||||||
|
exit(1)
|
||||||
|
input_file_names = [cache_file_path]
|
||||||
|
|
||||||
|
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||||
|
min_: np.array
|
||||||
|
max_: np.array
|
||||||
|
osm_data: OSMData
|
||||||
|
|
||||||
|
if input_file_names[0].name.endswith(".json"):
|
||||||
|
reader: OverpassReader = OverpassReader()
|
||||||
|
reader.parse_json_file(input_file_names[0])
|
||||||
|
|
||||||
|
osm_data = reader.osm_data
|
||||||
|
view_box = MinMax(
|
||||||
|
np.array(
|
||||||
|
(osm_data.boundary_box[0].min_, osm_data.boundary_box[1].min_)
|
||||||
|
),
|
||||||
|
np.array(
|
||||||
|
(osm_data.boundary_box[0].max_, osm_data.boundary_box[1].max_)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE]
|
||||||
|
osm_reader = OSMReader(is_full=is_full)
|
||||||
|
|
||||||
|
for file_name in input_file_names:
|
||||||
|
if not file_name.is_file():
|
||||||
|
print(f"Fatal: no such file: {file_name}.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
osm_reader.parse_osm_file(file_name)
|
||||||
|
|
||||||
|
osm_data = osm_reader.osm_data
|
||||||
|
|
||||||
|
if options.boundary_box:
|
||||||
|
view_box = MinMax(
|
||||||
|
np.array((boundary_box.bottom, boundary_box.left)),
|
||||||
|
np.array((boundary_box.top, boundary_box.right)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
view_box = osm_data.view_box
|
||||||
|
|
||||||
|
flinger: Flinger = Flinger(view_box, options.scale)
|
||||||
|
size: np.array = flinger.size
|
||||||
|
|
||||||
|
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||||
|
options.output_file_name, size=size
|
||||||
|
)
|
||||||
|
icon_extractor: ShapeExtractor = ShapeExtractor(
|
||||||
|
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
if options.level:
|
||||||
|
if options.level == "overground":
|
||||||
|
check_level = check_level_overground
|
||||||
|
elif options.level == "underground":
|
||||||
|
|
||||||
|
def check_level(x) -> bool:
|
||||||
|
"""Draw underground objects."""
|
||||||
|
return not check_level_overground(x)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def check_level(x) -> bool:
|
||||||
|
"""Draw objects on the specified level."""
|
||||||
|
return not check_level_number(x, float(options.level))
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def check_level(_) -> bool:
|
||||||
|
"""Draw objects on any level."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
constructor: Constructor = Constructor(
|
||||||
|
osm_data,
|
||||||
|
flinger,
|
||||||
|
scheme,
|
||||||
|
icon_extractor,
|
||||||
|
check_level,
|
||||||
|
options.mode,
|
||||||
|
options.seed,
|
||||||
|
)
|
||||||
|
constructor.construct()
|
||||||
|
|
||||||
|
painter: Map = Map(
|
||||||
|
overlap=options.overlap,
|
||||||
|
mode=options.mode,
|
||||||
|
label_mode=options.label_mode,
|
||||||
|
flinger=flinger,
|
||||||
|
svg=svg,
|
||||||
|
scheme=scheme,
|
||||||
|
)
|
||||||
|
painter.draw(constructor)
|
||||||
|
|
||||||
|
print(f"Writing output SVG to {options.output_file_name}...")
|
||||||
|
with open(options.output_file_name, "w") as output_file:
|
||||||
|
svg.write(output_file)
|
||||||
|
|
|
@ -181,7 +181,7 @@ def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ways",
|
"--ways",
|
||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
default=True,
|
default=False,
|
||||||
help="add style for ways and relations",
|
help="add style for ways and relations",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue