From 1b035300df2fbdfa545a449e7e9ab1e52025012e Mon Sep 17 00:00:00 2001 From: Sergey Vartanov Date: Fri, 12 Nov 2021 00:59:32 +0300 Subject: [PATCH] Refactor buildings and craters. --- map_machine/constructor.py | 6 +- map_machine/feature/building.py | 146 +++++++++++++++++ map_machine/feature/crater.py | 46 ++++++ map_machine/feature/direction.py | 97 ++++++++++++ map_machine/figure.py | 264 +------------------------------ 5 files changed, 293 insertions(+), 266 deletions(-) create mode 100644 map_machine/feature/building.py create mode 100644 map_machine/feature/crater.py diff --git a/map_machine/constructor.py b/map_machine/constructor.py index 113b1bd..f0678f7 100644 --- a/map_machine/constructor.py +++ b/map_machine/constructor.py @@ -11,11 +11,11 @@ import numpy as np from colour import Color from map_machine.color import get_gradient_color +from map_machine.feature.building import Building +from map_machine.feature.crater import Crater +from map_machine.feature.direction import DirectionSector from map_machine.feature.road import Road, Roads from map_machine.figure import ( - Building, - Crater, - DirectionSector, StyledFigure, Tree, ) diff --git a/map_machine/feature/building.py b/map_machine/feature/building.py new file mode 100644 index 0000000..092c8e7 --- /dev/null +++ b/map_machine/feature/building.py @@ -0,0 +1,146 @@ +""" +Buildings on the map. +""" +import numpy as np +from colour import Color +from svgwrite import Drawing +from svgwrite.container import Group +from svgwrite.path import Path +from typing import Any, Optional + +from map_machine.drawing import PathCommands +from map_machine.feature.direction import Segment +from map_machine.figure import Figure +from map_machine.geometry.flinger import Flinger +from map_machine.osm.osm_reader import OSMNode +from map_machine.scheme import Scheme, LineStyle + +BUILDING_HEIGHT_SCALE: float = 2.5 +BUILDING_MINIMAL_HEIGHT: float = 8.0 + + +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) + + style: dict[str, Any] = { + "fill": scheme.get_color("building_color").hex, + "stroke": scheme.get_color("building_border_color").hex, + } + self.line_style: LineStyle = LineStyle(style) + 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 + + levels: Optional[str] = self.get_float("building:levels") + if levels: + self.height = float(levels) * BUILDING_HEIGHT_SCALE + + levels: Optional[str] = self.get_float("building:min_level") + if levels: + self.min_height = float(levels) * BUILDING_HEIGHT_SCALE + + height: Optional[float] = self.get_length("height") + if height: + self.height = height + + height: Optional[float] = self.get_length("min_height") + if height: + self.min_height = height + + def draw(self, svg: Drawing, flinger: Flinger) -> None: + """Draw simple building shape.""" + path: Path = Path(d=self.get_path(flinger)) + path.update(self.line_style.style) + path.update({"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() / 3.0 + 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( + d=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.""" + shift_1: np.ndarray = np.array((0.0, -previous_height * scale)) + shift_2: np.ndarray = np.array((0.0, -height * scale)) + for segment in self.parts: + fill: Color + if height == 2.0: + fill = Color("#AAAAAA") + elif height == 4.0: + 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: Path = svg.path( + d=command, + fill=fill.hex, + stroke=fill.hex, + stroke_width=1, + stroke_linejoin="round", + ) + svg.add(path) + + 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])) + ) + path.update(self.line_style.style) + path.update({"stroke-linejoin": "round"}) + svg.add(path) diff --git a/map_machine/feature/crater.py b/map_machine/feature/crater.py new file mode 100644 index 0000000..29885fd --- /dev/null +++ b/map_machine/feature/crater.py @@ -0,0 +1,46 @@ +""" +Crater on the map. +""" +import numpy as np +from colour import Color +from svgwrite import Drawing + +from map_machine.geometry.flinger import Flinger +from map_machine.osm.osm_reader import Tagged + + +class Crater(Tagged): + """Volcano or impact crater on the map.""" + + def __init__( + self, tags: dict[str, str], coordinates: np.ndarray, point: np.ndarray + ) -> None: + super().__init__(tags) + self.coordinates: np.ndarray = coordinates + self.point: np.ndarray = point + + def draw(self, svg: Drawing, flinger: Flinger) -> None: + """Draw crater ridge.""" + scale: float = flinger.get_scale(self.coordinates) + assert "diameter" in self.tags + radius: float = float(self.tags["diameter"]) / 2.0 + radial_gradient = svg.radialGradient( + center=self.point + np.array((0.0, radius * scale / 7.0)), + r=radius * scale, + gradientUnits="userSpaceOnUse", + ) + color: Color = Color("#000000") + gradient = svg.defs.add(radial_gradient) + ( + gradient + .add_stop_color(0.0, color.hex, opacity=0.2) + .add_stop_color(0.7, color.hex, opacity=0.2) + .add_stop_color(1.0, color.hex, opacity=1.0) + ) # fmt: skip + circle = svg.circle( + self.point, + radius * scale, + fill=gradient.get_funciri(), + opacity=0.2, + ) + svg.add(circle) diff --git a/map_machine/feature/direction.py b/map_machine/feature/direction.py index eb2b3b7..b45a439 100644 --- a/map_machine/feature/direction.py +++ b/map_machine/feature/direction.py @@ -1,12 +1,17 @@ """ Direction tag support. """ +from colour import Color +from svgwrite import Drawing +from svgwrite.gradients import RadialGradient +from svgwrite.path import Path from typing import Iterator, Optional import numpy as np from portolan import middle from map_machine.drawing import PathCommands +from map_machine.osm.osm_reader import Tagged __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" @@ -155,3 +160,95 @@ class DirectionSet: if result == [False] * len(result): return False return None + + +class DirectionSector(Tagged): + """Sector that represents direction.""" + + def __init__(self, tags: dict[str, str], point: np.ndarray) -> None: + super().__init__(tags) + self.point: np.ndarray = point + + def draw(self, svg: Drawing, scheme) -> None: + """Draw gradient sector.""" + angle: Optional[float] = None + is_revert_gradient: bool = False + direction: str + direction_radius: float + direction_color: Color + + if self.get_tag("man_made") == "surveillance": + direction = self.get_tag("camera:direction") + if "camera:angle" in self.tags: + angle = float(self.get_tag("camera:angle")) + if "angle" in self.tags: + angle = float(self.get_tag("angle")) + direction_radius = 50.0 + direction_color = scheme.get_color("direction_camera_color") + elif self.get_tag("traffic_sign") == "stop": + direction = self.get_tag("direction") + direction_radius = 25.0 + direction_color = Color("red") + else: + direction = self.get_tag("direction") + direction_radius = 50.0 + direction_color = scheme.get_color("direction_view_color") + is_revert_gradient = True + + if not direction: + return + + point: np.ndarray = (self.point.astype(int)).astype(float) + + paths: Iterator[PathCommands] + if angle is not None: + paths = [Sector(direction, angle).draw(point, direction_radius)] + else: + paths = DirectionSet(direction).draw(point, direction_radius) + + for path in paths: + radial_gradient: RadialGradient = svg.radialGradient( + center=point, + r=direction_radius, + gradientUnits="userSpaceOnUse", + ) + gradient: RadialGradient = svg.defs.add(radial_gradient) + + if is_revert_gradient: + ( + gradient + .add_stop_color(0.0, direction_color.hex, opacity=0.0) + .add_stop_color(1.0, direction_color.hex, opacity=0.7) + ) # fmt: skip + else: + ( + gradient + .add_stop_color(0.0, direction_color.hex, opacity=0.4) + .add_stop_color(1.0, direction_color.hex, opacity=0.0) + ) # fmt: skip + + path_element: Path = svg.path( + d=["M", point] + path + ["L", point, "Z"], + fill=gradient.get_funciri(), + ) + svg.add(path_element) + + +class Segment: + """Closed line segment.""" + + def __init__(self, point_1: np.ndarray, point_2: np.ndarray) -> None: + self.point_1: np.ndarray = point_1 + self.point_2: np.ndarray = point_2 + + difference: np.ndarray = point_2 - point_1 + vector: np.ndarray = difference / np.linalg.norm(difference) + self.angle: float = ( + np.arccos(np.dot(vector, np.array((0.0, 1.0)))) / np.pi + ) + + def __lt__(self, other: "Segment") -> bool: + return ( + ((self.point_1 + self.point_2) / 2.0)[1] + < ((other.point_1 + other.point_2) / 2.0)[1] + ) # fmt: skip diff --git a/map_machine/figure.py b/map_machine/figure.py index d372e29..f55e4a3 100644 --- a/map_machine/figure.py +++ b/map_machine/figure.py @@ -1,17 +1,12 @@ """ Figures displayed on the map. """ -from typing import Any, Iterator, Optional +from typing import Optional import numpy as np from colour import Color from svgwrite import Drawing -from svgwrite.container import Group -from svgwrite.gradients import RadialGradient -from svgwrite.path import Path -from map_machine.drawing import PathCommands -from map_machine.feature.direction import DirectionSet, Sector from map_machine.geometry.flinger import Flinger from map_machine.osm.osm_reader import OSMNode, Tagged from map_machine.scheme import LineStyle, Scheme @@ -21,9 +16,6 @@ __email__ = "me@enzet.ru" from map_machine.geometry.vector import Polyline -BUILDING_HEIGHT_SCALE: float = 2.5 -BUILDING_MINIMAL_HEIGHT: float = 8.0 - class Figure(Tagged): """Some figure on the map: way or area.""" @@ -61,133 +53,6 @@ class Figure(Tagged): return path -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) - - style: dict[str, Any] = { - "fill": scheme.get_color("building_color").hex, - "stroke": scheme.get_color("building_border_color").hex, - } - self.line_style: LineStyle = LineStyle(style) - 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 - - levels: Optional[str] = self.get_float("building:levels") - if levels: - self.height = float(levels) * BUILDING_HEIGHT_SCALE - - levels: Optional[str] = self.get_float("building:min_level") - if levels: - self.min_height = float(levels) * BUILDING_HEIGHT_SCALE - - height: Optional[float] = self.get_length("height") - if height: - self.height = height - - height: Optional[float] = self.get_length("min_height") - if height: - self.min_height = height - - def draw(self, svg: Drawing, flinger: Flinger) -> None: - """Draw simple building shape.""" - path: Path = Path(d=self.get_path(flinger)) - path.update(self.line_style.style) - path.update({"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() / 3.0 - shift_1: np.ndarray = np.array((scale * self.min_height, 0)) - shift_2: np.ndarray = np.array((scale * self.height, 0)) - commands: str = self.get_path(flinger, shift_1) - path: Path = Path( - d=commands, fill="#000000", stroke="#000000", stroke_width=1 - ) - 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 - ) - building_shade.add(path) - - def draw_walls( - self, svg: Drawing, height: float, previous_height: float, scale: float - ) -> None: - """Draw building walls.""" - shift_1: np.ndarray = np.array((0, -previous_height * scale)) - shift_2: np.ndarray = np.array((0, -height * scale)) - for segment in self.parts: - fill: Color - 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: Path = svg.path( - d=command, - fill=fill.hex, - stroke=fill.hex, - stroke_width=1, - stroke_linejoin="round", - ) - svg.add(path) - - 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, -self.height * scale])) - ) - path.update(self.line_style.style) - path.update({"stroke-linejoin": "round"}) - svg.add(path) - - class StyledFigure(Figure): """Figure with stroke and fill style.""" @@ -202,43 +67,6 @@ class StyledFigure(Figure): self.line_style: LineStyle = line_style -class Crater(Tagged): - """Volcano or impact crater on the map.""" - - def __init__( - self, tags: dict[str, str], coordinates: np.ndarray, point: np.ndarray - ) -> None: - super().__init__(tags) - self.coordinates: np.ndarray = coordinates - self.point: np.ndarray = point - - def draw(self, svg: Drawing, flinger: Flinger) -> None: - """Draw crater ridge.""" - scale: float = flinger.get_scale(self.coordinates) - assert "diameter" in self.tags - radius: float = float(self.tags["diameter"]) / 2.0 - radial_gradient = svg.radialGradient( - center=self.point + np.array((0, radius * scale / 7)), - r=radius * scale, - gradientUnits="userSpaceOnUse", - ) - color: Color = Color("#000000") - gradient = svg.defs.add(radial_gradient) - ( - gradient - .add_stop_color(0, color.hex, opacity=0.2) - .add_stop_color(0.7, color.hex, opacity=0.2) - .add_stop_color(1, color.hex, opacity=1) - ) # fmt: skip - circle = svg.circle( - self.point, - radius * scale, - fill=gradient.get_funciri(), - opacity=0.2, - ) - svg.add(circle) - - class Tree(Tagged): """Tree on the map.""" @@ -269,96 +97,6 @@ class Tree(Tagged): svg.add(svg.circle(self.point, radius * scale, fill="#B89A74")) -class DirectionSector(Tagged): - """Sector that represents direction.""" - - def __init__(self, tags: dict[str, str], point: np.ndarray) -> None: - super().__init__(tags) - self.point: np.ndarray = point - - def draw(self, svg: Drawing, scheme: Scheme) -> None: - """Draw gradient sector.""" - angle: Optional[float] = None - is_revert_gradient: bool = False - direction: str - direction_radius: float - direction_color: Color - - if self.get_tag("man_made") == "surveillance": - direction = self.get_tag("camera:direction") - if "camera:angle" in self.tags: - angle = float(self.get_tag("camera:angle")) - if "angle" in self.tags: - angle = float(self.get_tag("angle")) - direction_radius = 50 - direction_color = scheme.get_color("direction_camera_color") - elif self.get_tag("traffic_sign") == "stop": - direction = self.get_tag("direction") - direction_radius = 25 - direction_color = Color("red") - else: - direction = self.get_tag("direction") - direction_radius = 50 - direction_color = scheme.get_color("direction_view_color") - is_revert_gradient = True - - if not direction: - return - - point: np.ndarray = (self.point.astype(int)).astype(float) - - paths: Iterator[PathCommands] - if angle is not None: - paths = [Sector(direction, angle).draw(point, direction_radius)] - else: - paths = DirectionSet(direction).draw(point, direction_radius) - - for path in paths: - radial_gradient: RadialGradient = svg.radialGradient( - center=point, - r=direction_radius, - gradientUnits="userSpaceOnUse", - ) - gradient: RadialGradient = 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_element: Path = svg.path( - d=["M", point] + path + ["L", point, "Z"], - fill=gradient.get_funciri(), - ) - svg.add(path_element) - - -class Segment: - """Closed line segment.""" - - def __init__(self, point_1: np.ndarray, point_2: np.ndarray) -> None: - self.point_1: np.ndarray = point_1 - self.point_2: np.ndarray = point_2 - - difference: np.ndarray = point_2 - point_1 - vector: np.ndarray = difference / np.linalg.norm(difference) - self.angle: float = np.arccos(np.dot(vector, np.array((0, 1)))) / np.pi - - def __lt__(self, other: "Segment") -> bool: - return ( - ((self.point_1 + self.point_2) / 2)[1] - < ((other.point_1 + other.point_2) / 2)[1] - ) # fmt: skip - - def is_clockwise(polygon: list[OSMNode]) -> bool: """ Return true if polygon nodes are in clockwise order.