Merge main.

This commit is contained in:
Sergey Vartanov 2022-04-13 22:17:51 +03:00
commit 155b1e21c7
112 changed files with 17854 additions and 5686 deletions

View file

@ -3,6 +3,7 @@ Simple OpenStreetMap renderer.
"""
import argparse
import logging
import sys
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Set
@ -13,18 +14,21 @@ from svgwrite.container import Group
from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect
from map_machine.boundary_box import BoundaryBox
from map_machine.constructor import Constructor
from map_machine.drawing import draw_text
from map_machine.feature.building import Building, draw_walls, BUILDING_SCALE
from map_machine.feature.road import Intersection, Road, RoadPart
from map_machine.figure import StyledFigure
from map_machine.flinger import Flinger
from map_machine.icon import ShapeExtractor
from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger
from map_machine.geometry.vector import Segment
from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm_getter import NetworkError, get_osm
from map_machine.osm_reader import OSMData, OSMNode
from map_machine.point import Occupied, Point
from map_machine.road import Intersection, Road, RoadPart
from map_machine.osm.osm_getter import NetworkError, get_osm
from map_machine.osm.osm_reader import OSMData, OSMNode
from map_machine.pictogram.icon import ShapeExtractor
from map_machine.pictogram.point import Occupied, Point
from map_machine.scheme import Scheme
from map_machine.ui import BuildingMode, progress_bar
from map_machine.ui.cli import BuildingMode
from map_machine.workspace import workspace
__author__ = "Sergey Vartanov"
@ -32,9 +36,7 @@ __email__ = "me@enzet.ru"
class Map:
"""
Map drawing.
"""
"""Map drawing."""
def __init__(
self,
@ -49,33 +51,32 @@ class Map:
self.configuration = configuration
self.background_color: Color = self.scheme.get_color("background_color")
if self.configuration.is_wireframe():
self.background_color: Color = Color("#111111")
if color := self.configuration.background_color():
self.background_color = color
def draw(self, constructor: Constructor) -> None:
"""Draw map."""
self.svg.add(
Rect((0, 0), self.flinger.size, fill=self.background_color)
Rect((0.0, 0.0), self.flinger.size, fill=self.background_color)
)
ways: List[StyledFigure] = sorted(
constructor.figures, key=lambda x: x.line_style.priority
)
ways_length: int = len(ways)
for index, way in enumerate(ways):
progress_bar(index, ways_length, step=10, text="Drawing ways")
logging.info("Drawing ways...")
for way in ways:
path_commands: str = way.get_path(self.flinger)
if path_commands:
path: SVGPath = SVGPath(d=path_commands)
path.update(way.line_style.style)
self.svg.add(path)
progress_bar(-1, 0, text="Drawing ways")
constructor.roads.draw(self.svg, self.flinger)
for tree in constructor.trees:
tree.draw(self.svg, self.flinger, self.scheme)
for tree in constructor.craters:
tree.draw(self.svg, self.flinger)
for crater in constructor.craters:
crater.draw(self.svg, self.flinger)
self.draw_buildings(constructor)
@ -97,22 +98,16 @@ class Map:
nodes: List[Point] = sorted(
constructor.points, key=lambda x: -x.priority
)
steps: int = len(nodes)
for index, node in enumerate(nodes):
progress_bar(index, steps * 3, step=10, text="Drawing main icons")
logging.info("Drawing main icons...")
for node in nodes:
node.draw_main_shapes(self.svg, occupied)
for index, point in enumerate(nodes):
progress_bar(
steps + index, steps * 3, step=10, text="Drawing extra icons"
)
logging.info("Drawing extra icons...")
for point in nodes:
point.draw_extra_shapes(self.svg, occupied)
for index, point in enumerate(nodes):
progress_bar(
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
)
logging.info("Drawing texts...")
for point in nodes:
if (
not self.configuration.is_wireframe()
and self.configuration.label_mode != LabelMode.NO
@ -121,7 +116,7 @@ class Map:
self.svg, occupied, self.configuration.label_mode
)
progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
self.draw_credits(constructor.flinger.size)
def draw_buildings(self, constructor: Constructor) -> None:
"""Draw buildings: shade, walls, and roof."""
@ -132,21 +127,36 @@ class Map:
building.draw(self.svg, self.flinger)
return
scale: float = self.flinger.get_scale() / 3.0
logging.info("Drawing buildings...")
scale: float = self.flinger.get_scale()
building_shade: Group = Group(opacity=0.1)
for building in constructor.buildings:
building.draw_shade(building_shade, self.flinger)
self.svg.add(building_shade)
previous_height: float = 0
count: int = len(constructor.heights)
for index, height in enumerate(sorted(constructor.heights)):
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:
walls: dict[Segment, Building] = {}
for building in constructor.buildings:
for part in building.parts:
walls[part] = building
sorted_walls = sorted(walls.keys())
previous_height: float = 0.0
for height in sorted(constructor.heights):
shift_1: np.ndarray = np.array(
(0.0, -previous_height * scale * BUILDING_SCALE)
)
shift_2: np.ndarray = np.array(
(0.0, -height * scale * BUILDING_SCALE)
)
for wall in sorted_walls:
building: Building = walls[wall]
if building.height < height or building.min_height >= height:
continue
building.draw_walls(self.svg, height, previous_height, scale)
draw_walls(self.svg, building, wall, height, shift_1, shift_2)
if self.configuration.draw_roofs:
for building in constructor.buildings:
@ -155,16 +165,14 @@ class Map:
previous_height = height
progress_bar(-1, count, step=1, text="Drawing buildings")
def draw_roads(self, roads: Iterator[Road]) -> None:
def draw_simple_roads(self, roads: Iterator[Road]) -> None:
"""Draw road as simple SVG path."""
nodes: Dict[OSMNode, Set[RoadPart]] = {}
for road in roads:
for index in range(len(road.outers[0]) - 1):
node_1: OSMNode = road.outers[0][index]
node_2: OSMNode = road.outers[0][index + 1]
for index in range(len(road.nodes) - 1):
node_1: OSMNode = road.nodes[index]
node_2: OSMNode = road.nodes[index + 1]
point_1: np.ndarray = self.flinger.fling(node_1.coordinates)
point_2: np.ndarray = self.flinger.fling(node_2.coordinates)
scale: float = self.flinger.get_scale(node_1.coordinates)
@ -179,17 +187,43 @@ class Map:
nodes[node_1].add(part_1)
nodes[node_2].add(part_2)
for node in nodes:
parts: Set[RoadPart] = nodes[node]
for node, parts in nodes.items():
if len(parts) < 4:
continue
intersection: Intersection = Intersection(list(parts))
intersection.draw(self.svg, True)
def draw_credits(self, size: np.ndarray):
def ui(arguments: argparse.Namespace) -> None:
for text, point in (
("Data: © OpenStreetMap contributors", np.array((15, 27))),
("Rendering: Map Machine", np.array((15, 15))),
):
for stroke_width, stroke, opacity in (
(3.0, Color("white"), 0.7),
(1.0, None, 1.0),
):
draw_text(
self.svg,
text,
size - point,
10,
Color("#888888"),
anchor="end",
stroke_width=stroke_width,
stroke=stroke,
opacity=opacity,
)
def fatal(message: str) -> None:
logging.fatal(message)
sys.exit(1)
def render_map(arguments: argparse.Namespace) -> None:
"""
Map Machine entry point.
Map rendering entry point.
:param arguments: command-line arguments
"""
@ -199,64 +233,65 @@ def ui(arguments: argparse.Namespace) -> None:
cache_path: Path = Path(arguments.cache)
cache_path.mkdir(parents=True, exist_ok=True)
# Compute boundary box
boundary_box: Optional[BoundaryBox] = None
input_file_names: List[Path] = []
if arguments.boundary_box:
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
elif arguments.coordinates and arguments.size:
coordinates: np.ndarray = np.array(
list(map(float, arguments.coordinates.split(",")))
)
if len(coordinates) != 2:
fatal("Wrong number of coordinates.")
width, height = np.array(list(map(float, arguments.size.split(","))))
boundary_box = BoundaryBox.from_coordinates(
coordinates, configuration.zoom_level, width, height
)
# Determine files
if arguments.input_file_names:
input_file_names = list(map(Path, arguments.input_file_names))
if arguments.boundary_box:
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
else:
if arguments.boundary_box:
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
elif arguments.coordinates and arguments.size:
coordinates: np.ndarray = np.array(
list(map(float, arguments.coordinates.split(",")))
)
width, height = np.array(
list(map(float, arguments.size.split(",")))
)
boundary_box = BoundaryBox.from_coordinates(
coordinates, configuration.zoom_level, width, height
)
else:
logging.fatal(
"Specify either --input, or --boundary-box, or --coordinates "
"and --size."
)
exit(1)
elif boundary_box:
try:
cache_file_path: Path = (
cache_path / f"{boundary_box.get_format()}.osm"
)
get_osm(boundary_box, cache_file_path)
input_file_names = [cache_file_path]
except NetworkError as e:
logging.fatal(e.message)
exit(1)
except NetworkError as error:
logging.fatal(error.message)
sys.exit(1)
else:
fatal(
"Specify either --input, or --boundary-box, or --coordinates and "
"--size."
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
min_: np.ndarray
max_: np.ndarray
osm_data: OSMData
# Get OpenStreetMap data
osm_data: OSMData = OSMData()
for input_file_name in input_file_names:
if not input_file_name.is_file():
logging.fatal(f"No such file: {input_file_name}.")
exit(1)
sys.exit(1)
if input_file_name.name.endswith(".json"):
osm_data.parse_overpass(input_file_name)
else:
osm_data.parse_osm_file(input_file_name)
view_box: BoundaryBox = boundary_box if boundary_box else osm_data.view_box
if not boundary_box:
boundary_box = osm_data.view_box
if not boundary_box:
boundary_box = osm_data.boundary_box
# Render
flinger: Flinger = Flinger(
view_box, arguments.zoom, osm_data.equator_length
boundary_box, arguments.zoom, osm_data.equator_length
)
size: np.ndarray = flinger.size
@ -265,6 +300,7 @@ def ui(arguments: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data=osm_data,
flinger=flinger,
@ -274,10 +310,10 @@ def ui(arguments: argparse.Namespace) -> None:
)
constructor.construct()
painter: Map = Map(
map_: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
)
painter.draw(constructor)
map_.draw(constructor)
logging.info(f"Writing output SVG to {arguments.output_file_name}...")
with open(arguments.output_file_name, "w", encoding="utf-8") as output_file: