Refactor entry point.

This commit is contained in:
Sergey Vartanov 2021-08-24 23:01:41 +03:00
parent cbfdb4de78
commit 9555a5d74f
9 changed files with 400 additions and 266 deletions

View file

@ -1,250 +1,8 @@
"""
Röntgen entry point.
Author: Sergey Vartanov (me@enzet.ru).
"""
import argparse
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)
from roentgen.main import main
if __name__ == "__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())
main()

111
roentgen/boundary_box.py Normal file
View 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
View 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}.")

View file

@ -121,6 +121,7 @@ class IconCollection:
by_name: bool = False,
color: Optional[Color] = None,
outline: bool = False,
outline_opacity: float = 1.0,
):
"""
: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 color: fill color
:param outline: if true, draw outline beneath the icon
:param outline_opacity: opacity of the outline
"""
if by_name:
@ -146,6 +148,7 @@ class IconCollection:
output_directory / get_file_name(icon),
color=color,
outline=outline,
outline_opacity=outline_opacity,
)
def draw_grid(

View file

@ -330,6 +330,7 @@ class ShapeSpecification:
point: np.array,
tags: dict[str, Any] = None,
outline: bool = False,
outline_opacity: float = 1.0,
) -> None:
"""
Draw icon shape into SVG file.
@ -338,6 +339,7 @@ class ShapeSpecification:
:param point: 2D position of the shape centre
:param tags: tags to be displayed as hint
:param outline: draw outline for the shape
:param outline_opacity: opacity of the outline
"""
scale: np.array = np.array((1, 1))
if self.flip_vertically:
@ -358,6 +360,7 @@ class ShapeSpecification:
"stroke": color.hex,
"stroke-width": 2.2,
"stroke-linejoin": "round",
"opacity": outline_opacity,
}
path.update(style)
if tags:
@ -431,6 +434,7 @@ class Icon:
file_name: Path,
color: Optional[Color] = None,
outline: bool = False,
outline_opacity: float = 1.0,
):
"""
Draw icon to the SVG file.
@ -438,13 +442,16 @@ class Icon:
:param file_name: output SVG file name
:param color: fill color
:param outline: if true, draw outline beneath the icon
:param outline_opacity: opacity of the outline
"""
svg: Drawing = Drawing(str(file_name), (16, 16))
for shape_specification in self.shape_specifications:
if 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:
if color:

63
roentgen/main.py Normal file
View 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()

View file

@ -100,7 +100,7 @@ class MapCSSWriter:
:param matcher: tag matcher of Röntgen scheme
:param prefix: tag prefix
:param opacity: icon opacity
:return:
:return: string representation of selector
"""
elements: dict[str, str] = {}
@ -190,7 +190,10 @@ def ui(options) -> None:
)
collection: IconCollection = IconCollection.from_scheme(scheme, extractor)
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(
scheme,

View file

@ -1,24 +1,29 @@
"""
Simple OpenStreetMap renderer.
"""
from pathlib import Path
from typing import Any, Iterator
import logging
import numpy as np
import svgwrite
from colour import Color
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.path import Path as SVGPath
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.figure import Road
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.road import Intersection, RoadPart
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"
__email__ = "me@enzet.ru"
@ -58,13 +63,13 @@ class Map:
ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
ways_length: int = len(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)
if path_commands:
path = Path(d=path_commands)
path = SVGPath(d=path_commands)
path.update(way.line_style.style)
self.svg.add(path)
ui.progress_bar(-1, 0, text="Drawing ways")
progress_bar(-1, 0, text="Drawing ways")
roads: Iterator[Road] = sorted(
constructor.roads, key=lambda x: x.matcher.priority
@ -95,19 +100,17 @@ class Map:
steps: int = len(nodes)
for index, node in enumerate(nodes):
ui.progress_bar(
index, steps * 3, step=10, text="Drawing main icons"
)
progress_bar(index, steps * 3, step=10, text="Drawing main icons")
node.draw_main_shapes(self.svg, occupied)
for index, point in enumerate(nodes):
ui.progress_bar(
progress_bar(
steps + index, steps * 3, step=10, text="Drawing extra icons"
)
point.draw_extra_shapes(self.svg, occupied)
for index, point in enumerate(nodes):
ui.progress_bar(
progress_bar(
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
)
if (
@ -116,7 +119,7 @@ class Map:
):
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:
"""Draw buildings: shade, walls, and roof."""
@ -129,7 +132,7 @@ class Map:
previous_height: float = 0
count: int = len(constructor.heights)
for index, height in enumerate(sorted(constructor.heights)):
ui.progress_bar(index, count, step=1, text="Drawing buildings")
progress_bar(index, count, step=1, text="Drawing buildings")
fill: Color()
for building in constructor.buildings:
if building.height < height or building.min_height > height:
@ -142,7 +145,7 @@ class Map:
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(
self, road: Road, color: Color, extra_width: float = 0
@ -157,7 +160,7 @@ class Map:
width = road.matcher.default_width
scale = self.flinger.get_scale(road.outers[0][0].coordinates)
path_commands: str = road.get_path(self.flinger)
path = Path(d=path_commands)
path = SVGPath(d=path_commands)
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
@ -196,9 +199,8 @@ class Map:
parts = nodes[node]
if len(parts) < 4:
continue
scale: float = self.flinger.get_scale(node.coordinates)
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):
@ -237,3 +239,130 @@ def check_level_overground(tags: dict[str, Any]) -> bool:
if "parking" in tags and tags["parking"] == "underground":
return False
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)

View file

@ -181,7 +181,7 @@ def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--ways",
action=argparse.BooleanOptionalAction,
default=True,
default=False,
help="add style for ways and relations",
)
parser.add_argument(