map-machine/map_machine/feature/building.py
Sergey Vartanov d5ef4aba4e Merge main.
2022-09-07 02:47:46 +03:00

226 lines
7.5 KiB
Python

"""Buildings on the map."""
import numpy as np
import svgwrite
from colour import Color
from svgwrite import Drawing
from svgwrite.container import Group
from svgwrite.path import Path
from typing import Dict, List
from map_machine.drawing import PathCommands
from map_machine.figure import Figure
from map_machine.geometry.flinger import Flinger
from map_machine.geometry.vector import Segment
from map_machine.osm.osm_reader import OSMNode
from map_machine.scheme import Scheme
BUILDING_MINIMAL_HEIGHT: float = 8.0
BUILDING_SCALE: float = 0.33
LEVEL_HEIGHT: float = 2.5
SHADE_SCALE: float = 0.4
class Building(Figure):
"""Building on the map."""
def __init__(
self,
tags: Dict[str, str],
inners: List[List[OSMNode]],
outers: List[List[OSMNode]],
flinger: Flinger,
scheme: Scheme,
) -> None:
super().__init__(tags, inners, outers)
self.is_construction: bool = (
tags.get("building") == "construction"
or tags.get("construction") == "yes"
)
self.has_walls: bool = tags.get("building") != "roof"
if self.is_construction:
self.fill: Color = scheme.get_color("building_construction_color")
self.stroke: Color = scheme.get_color(
"building_construction_border_color"
)
else:
if color := tags.get("roof:colour"):
self.fill = scheme.get_color(color)
self.stroke: Color = Color(self.fill)
self.stroke.set_luminance(self.fill.get_luminance() * 0.85)
else:
self.fill: Color = scheme.get_color("building_color")
self.stroke: Color = scheme.get_color("building_border_color")
self.parts: List[Segment] = []
for nodes in self.inners + self.outers:
for i in range(len(nodes) - 1):
flung_1: np.ndarray = flinger.fling(nodes[i].coordinates)
flung_2: np.ndarray = flinger.fling(nodes[i + 1].coordinates)
self.parts.append(Segment(flung_1, flung_2))
self.parts = sorted(self.parts)
self.height: float = BUILDING_MINIMAL_HEIGHT
self.min_height: float = 0.0
self.wall_color: Color
if self.is_construction:
self.wall_color = scheme.get_color("wall_construction_color")
else:
self.wall_color = scheme.get_color("wall_color")
if material := tags.get("building:material"):
if material in scheme.material_colors:
self.wall_color = Color(scheme.material_colors[material])
if color := tags.get("building:colour"):
self.wall_color = scheme.get_color(color)
if color := tags.get("colour"):
self.wall_color = scheme.get_color(color)
self.wall_bottom_color_1: Color = Color(self.wall_color)
self.wall_bottom_color_1.set_luminance(
self.wall_color.get_luminance() * 0.70
)
self.wall_bottom_color_2: Color = Color(self.wall_color)
self.wall_bottom_color_2.set_luminance(
self.wall_color.get_luminance() * 0.85
)
if levels := self.get_float("building:levels"):
self.height = BUILDING_MINIMAL_HEIGHT + levels * LEVEL_HEIGHT
if levels := self.get_float("building:min_level"):
self.min_height = BUILDING_MINIMAL_HEIGHT + levels * LEVEL_HEIGHT
if height := self.get_length("height"):
self.height = BUILDING_MINIMAL_HEIGHT + height
if height := self.get_length("min_height"):
self.min_height = BUILDING_MINIMAL_HEIGHT + height
def draw(self, svg: Drawing, flinger: Flinger) -> None:
"""Draw simple building shape."""
path: Path = Path(
d=self.get_path(flinger),
stroke=self.stroke.hex,
fill=self.fill.hex,
stroke_linejoin="round",
)
svg.add(path)
def draw_shade(self, building_shade: Group, flinger: Flinger) -> None:
"""Draw shade casted by the building."""
scale: float = flinger.get_scale() * SHADE_SCALE
shift_1: np.ndarray = np.array((scale * self.min_height, 0.0))
shift_2: np.ndarray = np.array((scale * self.height, 0.0))
commands: str = self.get_path(flinger, shift_1)
path: Path = Path(
commands, fill="#000000", stroke="#000000", stroke_width=1.0
)
building_shade.add(path)
for nodes in self.inners + self.outers:
for i in range(len(nodes) - 1):
flung_1 = flinger.fling(nodes[i].coordinates)
flung_2 = flinger.fling(nodes[i + 1].coordinates)
command: PathCommands = [
"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 = Path(
command, fill="#000000", stroke="#000000", stroke_width=1.0
)
building_shade.add(path)
def draw_walls(
self, svg: Drawing, height: float, previous_height: float, scale: float
) -> None:
"""Draw building walls."""
if not self.has_walls:
return
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 segment in self.parts:
draw_walls(svg, self, segment, height, shift_1, shift_2)
def draw_roof(self, svg: Drawing, flinger: Flinger, scale: float) -> None:
"""Draw building roof."""
path: Path = Path(
d=self.get_path(
flinger, np.array([0.0, -self.height * scale * BUILDING_SCALE])
),
stroke=self.stroke,
fill="none" if self.is_construction else self.fill.hex,
stroke_linejoin="round",
)
svg.add(path)
def draw_walls(
svg: svgwrite.Drawing,
building: Building,
segment: Segment,
height: float,
shift_1: np.ndarray,
shift_2: np.ndarray,
):
"""
Draw walls for buildings as a quadrangle.
Color of the wall is based on illumination.
"""
color: Color
if building.is_construction:
color_part: float = segment.angle * 0.2
color = Color(
rgb=(
building.wall_color.get_red() + color_part,
building.wall_color.get_green() + color_part,
building.wall_color.get_blue() + color_part,
)
)
elif height <= 0.25 / BUILDING_SCALE:
color = building.wall_bottom_color_1
elif height <= 0.5 / BUILDING_SCALE:
color = building.wall_bottom_color_2
else:
color_part: float = segment.angle * 0.2 - 0.1
color = Color(
rgb=(
max(min(building.wall_color.get_red() + color_part, 1), 0),
max(min(building.wall_color.get_green() + color_part, 1), 0),
max(min(building.wall_color.get_blue() + color_part, 1), 0),
)
)
command: PathCommands = [
"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: Path = Path(
d=command,
fill=color.hex,
stroke=color.hex,
stroke_width=1,
stroke_linejoin="round",
)
svg.add(path)