"""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)