mirror of
https://github.com/enzet/map-machine.git
synced 2025-06-10 23:01:53 +02:00
Merge main.
This commit is contained in:
commit
155b1e21c7
112 changed files with 17854 additions and 5686 deletions
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue