mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-04 20:56:46 +02:00
417 lines
14 KiB
Python
417 lines
14 KiB
Python
"""
|
|
Simple OpenStreetMap renderer.
|
|
"""
|
|
from typing import Any, Dict, Iterator, Set
|
|
|
|
import numpy as np
|
|
import svgwrite
|
|
from colour import Color
|
|
from svgwrite.container import Group
|
|
from svgwrite.path import Path
|
|
from svgwrite.shapes import Rect
|
|
|
|
from roentgen import ui
|
|
from roentgen.constructor import Constructor
|
|
from roentgen.direction import DirectionSet, Sector
|
|
from roentgen.figure import Road
|
|
from roentgen.flinger import Flinger
|
|
from roentgen.icon import ShapeExtractor
|
|
from roentgen.osm_reader import Map, OSMNode
|
|
from roentgen.point import Occupied
|
|
from roentgen.road import Intersection, RoadPart
|
|
from roentgen.scheme import Scheme
|
|
|
|
__author__ = "Sergey Vartanov"
|
|
__email__ = "me@enzet.ru"
|
|
|
|
AUTHOR_MODE = "user-coloring"
|
|
CREATION_TIME_MODE = "time"
|
|
|
|
|
|
class Painter:
|
|
"""
|
|
Map drawing.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
map_: Map,
|
|
flinger: Flinger,
|
|
svg: svgwrite.Drawing,
|
|
icon_extractor: ShapeExtractor,
|
|
scheme: Scheme,
|
|
overlap: int = 12,
|
|
mode: str = "normal",
|
|
label_mode: str = "main",
|
|
):
|
|
self.overlap: int = overlap
|
|
self.mode: str = mode
|
|
self.label_mode: str = label_mode
|
|
|
|
self.map_: Map = map_
|
|
self.flinger: Flinger = flinger
|
|
self.svg: svgwrite.Drawing = svg
|
|
self.icon_extractor = icon_extractor
|
|
self.scheme: Scheme = scheme
|
|
|
|
self.background_color: Color = self.scheme.get_color("background_color")
|
|
if self.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
|
|
self.background_color: Color = Color("#111111")
|
|
|
|
def draw(self, constructor: Constructor) -> None:
|
|
"""
|
|
Draw map.
|
|
"""
|
|
self.svg.add(
|
|
Rect((0, 0), self.flinger.size, fill=self.background_color)
|
|
)
|
|
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")
|
|
path_commands: str = way.get_path(self.flinger)
|
|
if path_commands:
|
|
path = Path(d=path_commands)
|
|
path.update(way.line_style.style)
|
|
self.svg.add(path)
|
|
ui.progress_bar(-1, 0, text="Drawing ways")
|
|
|
|
roads: Iterator[Road] = sorted(
|
|
constructor.roads, key=lambda x: x.matcher.priority
|
|
)
|
|
for road in roads:
|
|
self.draw_road(road, road.matcher.border_color, 2)
|
|
for road in roads:
|
|
self.draw_road(road, road.matcher.color)
|
|
|
|
self.draw_trees(constructor)
|
|
self.draw_buildings(constructor)
|
|
self.draw_direction(constructor)
|
|
|
|
# All other points
|
|
|
|
if self.overlap == 0:
|
|
occupied = None
|
|
else:
|
|
occupied = Occupied(
|
|
self.flinger.size[0], self.flinger.size[1], self.overlap
|
|
)
|
|
|
|
nodes = sorted(constructor.points, key=lambda x: -x.priority)
|
|
steps: int = len(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(
|
|
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(
|
|
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(
|
|
steps * 2 + index, steps * 3, step=10, text="Drawing texts"
|
|
)
|
|
if (
|
|
self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]
|
|
and self.label_mode != "no"
|
|
):
|
|
point.draw_texts(self.svg, occupied, self.label_mode)
|
|
|
|
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:
|
|
"""
|
|
Draw buildings: shade, walls, and roof.
|
|
"""
|
|
# Draw shade.
|
|
|
|
building_shade: Group = Group(opacity=0.1)
|
|
scale: float = self.flinger.get_scale() / 3.0
|
|
for building in constructor.buildings:
|
|
shift_1 = np.array((scale * building.min_height, 0))
|
|
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)
|
|
|
|
# Draw buildings.
|
|
|
|
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")
|
|
fill: Color()
|
|
for way in constructor.buildings:
|
|
if way.height < height or way.min_height > height:
|
|
continue
|
|
shift_1 = [0, -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 = (
|
|
"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 = 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
|
|
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(
|
|
self, road: Road, color: Color, extra_width: float = 0
|
|
) -> None:
|
|
"""
|
|
Draw road as simple SVG path.
|
|
"""
|
|
self.flinger.get_scale()
|
|
if road.width is not None:
|
|
width = road.width
|
|
else:
|
|
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)
|
|
style: Dict[str, Any] = {
|
|
"fill": "none",
|
|
"stroke": color.hex,
|
|
"stroke-linecap": "round",
|
|
"stroke-linejoin": "round",
|
|
"stroke-width": scale * width + extra_width,
|
|
}
|
|
path.update(style)
|
|
self.svg.add(path)
|
|
|
|
def draw_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]
|
|
point_1: np.array = self.flinger.fling(node_1.coordinates)
|
|
point_2: np.array = self.flinger.fling(node_2.coordinates)
|
|
scale: float = self.flinger.get_scale(node_1.coordinates)
|
|
part_1: RoadPart = RoadPart(point_1, point_2, road.lanes, scale)
|
|
part_2: RoadPart = RoadPart(point_2, point_1, road.lanes, scale)
|
|
# part_1.draw_normal(self.svg)
|
|
|
|
for node in node_1, node_2:
|
|
if node not in nodes:
|
|
nodes[node] = set()
|
|
|
|
nodes[node_1].add(part_1)
|
|
nodes[node_2].add(part_2)
|
|
|
|
for node in nodes:
|
|
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)
|
|
|
|
|
|
def check_level_number(tags: Dict[str, Any], level: float):
|
|
"""
|
|
Check if element described by tags is no the specified level.
|
|
"""
|
|
if "level" in tags:
|
|
levels = map(float, tags["level"].replace(",", ".").split(";"))
|
|
if level not in levels:
|
|
return False
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_level_overground(tags: Dict[str, Any]) -> bool:
|
|
"""
|
|
Check if element described by tags is overground.
|
|
"""
|
|
if "level" in tags:
|
|
try:
|
|
levels = map(float, tags["level"].replace(",", ".").split(";"))
|
|
for level in levels:
|
|
if level <= 0:
|
|
return False
|
|
except ValueError:
|
|
pass
|
|
if "layer" in tags:
|
|
try:
|
|
levels = map(float, tags["layer"].replace(",", ".").split(";"))
|
|
for level in levels:
|
|
if level <= 0:
|
|
return False
|
|
except ValueError:
|
|
pass
|
|
if "parking" in tags and tags["parking"] == "underground":
|
|
return False
|
|
return True
|