Fix building height computing.

Add parsing for min_heigth and building:min_level tags; fix shadow
generation.
This commit is contained in:
Sergey Vartanov 2021-06-23 01:09:01 +03:00
parent f6d44b97bd
commit 41da1d73b8
4 changed files with 82 additions and 31 deletions

View file

@ -148,14 +148,15 @@ class Constructor:
self.buildings: List[Building] = [] self.buildings: List[Building] = []
self.roads: List[Road] = [] self.roads: List[Road] = []
self.levels: Set[float] = {0.5, 1.0} self.heights: Set[float] = {2, 4}
def add_building(self, building: Building) -> None: def add_building(self, building: Building) -> None:
""" """
Add building and update levels. Add building and update levels.
""" """
self.buildings.append(building) self.buildings.append(building)
self.levels.add(building.get_levels()) self.heights.add(building.height)
self.heights.add(building.min_height)
def construct(self) -> None: def construct(self) -> None:
""" """
@ -216,7 +217,7 @@ class Constructor:
line_styles: List[LineStyle] = self.scheme.get_style(line.tags, scale) line_styles: List[LineStyle] = self.scheme.get_style(line.tags, scale)
if "building" in line.tags: if "building:part" in line.tags or "building" in line.tags:
self.add_building( self.add_building(
Building(line.tags, inners, outers, self.flinger, self.scheme) Building(line.tags, inners, outers, self.flinger, self.scheme)
) )

View file

@ -80,14 +80,24 @@ class Building(Figure):
self.parts = sorted(self.parts) self.parts = sorted(self.parts)
def get_levels(self) -> float: self.height: float = 8.0
""" self.min_height: float = 0.0
Get building level number.
""" levels: Optional[str] = self.get_float("building:levels")
try: if levels:
return max(3.0, float(self.get_tag("building:levels"))) self.height = max(8.0, float(levels) * 2.5)
except (ValueError, TypeError):
return 3 levels: Optional[str] = self.get_float("building:min_level")
if levels:
self.min_height = max(8.0, float(levels) * 2.5)
height: Optional[str] = self.get_length("height")
if height:
self.height = max(8.0, height)
height: Optional[str] = self.get_length("min_height")
if height:
self.min_height = max(8.0, height)
class StyledFigure(Figure): class StyledFigure(Figure):

View file

@ -160,40 +160,49 @@ class Painter:
self.flinger.get_scale(node.coordinates), self.flinger.get_scale(node.coordinates),
fill="#B89A74")) fill="#B89A74"))
def draw_buildings(self, constructor) -> None: def draw_buildings(self, constructor: Constructor) -> None:
""" """
Draw buildings: shade, walls, and roof. Draw buildings: shade, walls, and roof.
""" """
# Draw shade.
building_shade: Group = Group(opacity=0.1) building_shade: Group = Group(opacity=0.1)
length: float = self.flinger.get_scale() scale: float = self.flinger.get_scale() / 3.0
for building in constructor.buildings: for building in constructor.buildings:
shift = np.array((length * building.get_levels(), 0)) 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 nodes in building.inners + building.outers:
for i in range(len(nodes) - 1): # type: int for i in range(len(nodes) - 1): # type: int
flung_1 = self.flinger.fling(nodes[i].coordinates) flung_1 = self.flinger.fling(nodes[i].coordinates)
flung_2 = self.flinger.fling(nodes[i + 1].coordinates) flung_2 = self.flinger.fling(nodes[i + 1].coordinates)
building_shade.add(Path( building_shade.add(Path(
("M", flung_1, "L", flung_2, np.add(flung_2, shift), ("M", np.add(flung_1, shift_1), "L",
np.add(flung_1, shift), "Z"), np.add(flung_2, shift_1), np.add(flung_2, shift_2),
np.add(flung_1, shift_2), "Z"),
fill="#000000", stroke="#000000", stroke_width=1)) fill="#000000", stroke="#000000", stroke_width=1))
self.svg.add(building_shade) self.svg.add(building_shade)
# Draw buildings. # Draw buildings.
previous_level: float = 0
level_height: float = self.flinger.get_scale() previous_height: float = 0
level_count: int = len(constructor.levels) count: int = len(constructor.heights)
for index, level in enumerate(sorted(constructor.levels)): for index, height in enumerate(sorted(constructor.heights)):
ui.progress_bar( ui.progress_bar(index, count, step=1, text="Drawing buildings")
index, level_count, step=1, text="Drawing buildings")
fill: Color() fill: Color()
for way in constructor.buildings: for way in constructor.buildings:
if way.get_levels() < level: if way.height < height or way.min_height > height:
continue continue
shift_1 = [0, -previous_level * level_height] shift_1 = [0, -previous_height * scale]
shift_2 = [0, -level * level_height] shift_2 = [0, -height * scale]
for segment in way.parts: for segment in way.parts:
if level == 0.5: if height == 2:
fill = Color("#AAAAAA") fill = Color("#AAAAAA")
elif level == 1: elif height == 4:
fill = Color("#C3C3C3") fill = Color("#C3C3C3")
else: else:
color_part: float = 0.8 + segment.angle * 0.2 color_part: float = 0.8 + segment.angle * 0.2
@ -211,16 +220,16 @@ class Painter:
# Draw building roofs. # Draw building roofs.
for way in constructor.buildings: for way in constructor.buildings:
if way.get_levels() == level: if way.height == height:
shift = np.array([0, -way.get_levels() * level_height]) shift = np.array([0, -way.height * scale])
path_commands: str = way.get_path(self.flinger, shift) path_commands: str = way.get_path(self.flinger, shift)
path = Path(d=path_commands, opacity=1) path = Path(d=path_commands, opacity=1)
path.update(way.line_style.style) path.update(way.line_style.style)
path.update({"stroke-linejoin": "round"}) path.update({"stroke-linejoin": "round"})
self.svg.add(path) self.svg.add(path)
previous_level = level previous_height = height
ui.progress_bar(-1, level_count, step=1, text="Drawing buildings") ui.progress_bar(-1, count, step=1, text="Drawing buildings")
def draw_direction(self, constructor) -> None: def draw_direction(self, constructor) -> None:
""" """

View file

@ -2,6 +2,7 @@
Reading OpenStreetMap data from XML file. Reading OpenStreetMap data from XML file.
""" """
import json import json
import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@ -17,6 +18,16 @@ __email__ = "me@enzet.ru"
OSM_TIME_PATTERN: str = "%Y-%m-%dT%H:%M:%SZ" OSM_TIME_PATTERN: str = "%Y-%m-%dT%H:%M:%SZ"
METERS_PATTERN = re.compile("(?P<value>\\d*\\.?\\d*)( )?m")
KILOMETERS_PATTERN = re.compile("(?P<value>\\d*\\.?\\d*)( )?km")
def parse_float(string: str) -> Optional[float]:
try:
return float(string)
except (TypeError, ValueError):
return None
class Tagged: class Tagged:
""" """
@ -37,6 +48,26 @@ class Tagged:
return self.tags[key] return self.tags[key]
return None return None
def get_float(self, key: str) -> Optional[float]:
if key in self.tags:
return parse_float(self.tags[key])
return None
def get_length(self, key: str) -> Optional[float]:
"""
Get length in meters.
"""
if key in self.tags:
value: str = self.tags[key]
matcher = METERS_PATTERN.match(value)
if matcher:
return parse_float(matcher.group("value"))
matcher = KILOMETERS_PATTERN.match(value)
if matcher:
return parse_float(matcher.group("value")) * 1000
return self.get_float(key)
return None
class OSMNode(Tagged): class OSMNode(Tagged):
""" """