From acf0ee45d928dc13c3e08a56543bd47a80771ae7 Mon Sep 17 00:00:00 2001 From: Sergey Vartanov Date: Sat, 3 Oct 2020 00:29:13 +0300 Subject: [PATCH] Support Overpass, overlaps, tree circumference. --- roentgen/grid.py | 73 +++++++++++++++++----------- roentgen/mapper.py | 115 ++++++++++++++++++++++++++------------------- roentgen/point.py | 54 +++++++++++---------- roentgen/ui.py | 3 +- test/test_icons.py | 6 +-- 5 files changed, 147 insertions(+), 104 deletions(-) diff --git a/roentgen/grid.py b/roentgen/grid.py index 22f2846..bd0d4b9 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -4,6 +4,7 @@ Icon grid drawing. Author: Sergey Vartanov (me@enzet.ru). """ import numpy as np +from colour import Color from svgwrite import Drawing from typing import List, Dict, Any, Set @@ -11,32 +12,24 @@ from roentgen.icon import Icon, IconExtractor from roentgen.scheme import Scheme -def draw_grid(step: float = 24, columns: int = 16): +def draw_all_icons(output_file_name: str, columns: int = 16, step: float = 24): """ Draw all possible icon combinations in grid. - :param step: horizontal and vertical distance between icons + :param output_file_name: output SVG file name for icon grid :param columns: the number of columns in grid + :param step: horizontal and vertical distance between icons """ tags_file_name: str = "data/tags.yml" - scheme: Scheme = Scheme(tags_file_name) - icons_file_name: str = "icons/icons.svg" - icon_grid_file_name: str = "icon_grid.svg" - - width: float = step * columns - point: np.array = np.array((step / 2, step / 2)) - to_draw: List[Set[str]] = [] for element in scheme.icons: # type: Dict[str, Any] - if "icon" in element: - if set(element["icon"]) not in to_draw: - to_draw.append(set(element["icon"])) - if "add_icon" in element: - if set(element["add_icon"]) not in to_draw: - to_draw.append(set(element["add_icon"])) + if "icon" in element and set(element["icon"]) not in to_draw: + to_draw.append(set(element["icon"])) + if "add_icon" in element and set(element["add_icon"]) not in to_draw: + to_draw.append(set(element["add_icon"])) if "over_icon" not in element: continue if "under_icon" in element: @@ -59,16 +52,44 @@ def draw_grid(step: float = 24, columns: int = 16): element["over_icon"]) if (icon_2_id != icon_3_id and icon_2_id != icon_id and icon_3_id != icon_id and - (current_set not in to_draw)): + current_set not in to_draw): to_draw.append(current_set) - number: int = 0 - - icons: List[List[Icon]] = [] - + icons_file_name: str = "icons/icons.svg" extractor: IconExtractor = IconExtractor(icons_file_name) - for icons_to_draw in to_draw: + specified_ids: Set[str] = set() + + for icons_to_draw in to_draw: # type: List[str] + specified_ids |= icons_to_draw + print( + "Icons with no tag specification: \n " + + ", ".join(sorted(extractor.icons.keys() - specified_ids)) + ".") + + draw_grid(output_file_name, to_draw, extractor, columns, step) + + +def draw_grid( + file_name: str, combined_icon_ids: List[Set[str]], + extractor: IconExtractor, columns: int = 16, step: float = 24, + color=Color("#444444")): + """ + Draw icons in the form of table + + :param file_name: output SVG file name + :param combined_icon_ids: list of set of icon string identifiers + :param extractor: icon extractor that generates icon SVG path commands using + its string identifier + :param columns: number of columns in grid + :param step: horizontal and vertical distance between icons in grid + :return: + """ + point: np.array = np.array((step / 2, step / 2)) + width: float = step * columns + number: int = 0 + icons: List[List[Icon]] = [] + + for icons_to_draw in combined_icon_ids: # type: Set[str] found: bool = False icon_set: List[Icon] = [] for icon_id in icons_to_draw: # type: str @@ -81,19 +102,17 @@ def draw_grid(step: float = 24, columns: int = 16): number += 1 height: int = int(int(number / (width / step) + 1) * step) - - svg: Drawing = Drawing(icon_grid_file_name, (width, height)) - + svg: Drawing = Drawing(file_name, (width, height)) svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF")) for combined_icon in icons: # type: List[Icon] - background_color, foreground_color = "#FFFFFF", "#444444" + background_color = "#FFFFFF" svg.add(svg.rect( point - np.array((-10, -10)), (20, 20), fill=background_color)) for icon in combined_icon: # type: Icon path = icon.get_path(svg, point) - path.update({"fill": foreground_color}) + path.update({"fill": color.hex}) svg.add(path) point += np.array((step, 0)) if point[0] > width - 8: @@ -103,5 +122,5 @@ def draw_grid(step: float = 24, columns: int = 16): print(f"Icons: {number}.") - with open(icon_grid_file_name, "w") as output_file: + with open(file_name, "w") as output_file: svg.write(output_file) diff --git a/roentgen/mapper.py b/roentgen/mapper.py index 0cd68a9..ef673f0 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -21,10 +21,10 @@ from roentgen.constructor import ( Constructor, Figure, Building, Segment) from roentgen.point import Point, Occupied from roentgen.flinger import Flinger -from roentgen.grid import draw_grid +from roentgen.grid import draw_all_icons from roentgen.icon import Icon, IconExtractor from roentgen.osm_getter import get_osm -from roentgen.osm_reader import Map, OSMReader +from roentgen.osm_reader import Map, OSMReader, OverpassReader from roentgen.scheme import Scheme from roentgen.direction import DirectionSet, Sector from roentgen.util import MinMax @@ -75,6 +75,31 @@ class Painter: self.svg.add(path) ui.progress_bar(-1, 0, text="Drawing ways") + # Trees + + for node in constructor.nodes: + if not (node.get_tag("natural") == "tree" and + ("diameter_crown" in node.tags or + "circumference" in node.tags)): + continue + 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 * self.flinger.get_scale(node.coordinates), + fill=self.scheme.get_color("evergreen_color"), + opacity=opacity)) + self.svg.add(self.svg.circle( + node.point, + float(node.tags["circumference"]) / 2 / np.pi * + self.flinger.get_scale(node.coordinates), + fill="#B89A74")) + # Draw building shade. building_shade: Group = Group(opacity=0.1) @@ -141,26 +166,6 @@ class Painter: ui.progress_bar(-1, level_count, step=1, text="Drawing buildings") - # Trees - - for node in constructor.nodes: - if not (node.get_tag("natural") == "tree" and - ("diameter_crown" in node.tags or - "circumference" in node.tags)): - continue - if "circumference" in node.tags: - self.svg.add(self.svg.circle( - node.point, - float(node.tags["circumference"]) * - self.flinger.get_scale(node.coordinates) / 2, - fill="#AAAA88", opacity=0.3)) - if "diameter_crown" in node.tags: - self.svg.add(self.svg.circle( - node.point, - float(node.tags["diameter_crown"]) * - self.flinger.get_scale(node.coordinates) / 2, - fill=self.scheme.get_color("evergreen"), opacity=0.3)) - # Directions for node in constructor.nodes: # type: Point @@ -219,7 +224,11 @@ class Painter: # All other points - occupied = Occupied(self.flinger.size[0], self.flinger.size[1]) + if self.overlap == 0: + occupied = None + else: + occupied = Occupied( + self.flinger.size[0], self.flinger.size[1], self.overlap) nodes = sorted(constructor.nodes, key=lambda x: x.layer) for index, node in enumerate(nodes): # type: int, Point @@ -286,7 +295,7 @@ def main(argv) -> None: """ if len(argv) == 2: if argv[1] == "grid": - draw_grid() + draw_all_icons("icon_grid.svg") return options: argparse.Namespace = ui.parse_options(argv) @@ -306,31 +315,40 @@ def main(argv) -> None: ui.error("cannot download OSM data") input_file_name = [os.path.join("map", options.boundary_box + ".osm")] - boundary_box = list(map( - lambda x: float(x.replace('m', '-')), options.boundary_box.split(','))) - - full = False # Full keys getting - - if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]: - full = True - - osm_reader = OSMReader() - - for file_name in input_file_name: - if not os.path.isfile(file_name): - print("Fatal: no such file: " + file_name + ".") - sys.exit(1) - - osm_reader.parse_osm_file( - file_name, parse_ways=options.draw_ways, - parse_relations=options.draw_ways, full=full) - - map_: Map = osm_reader.map_ - scheme: Scheme = Scheme(TAGS_FILE_NAME) - min1: np.array = np.array((boundary_box[1], boundary_box[0])) - max1: np.array = np.array((boundary_box[3], boundary_box[2])) + if input_file_name[0].endswith(".json"): + reader: OverpassReader = OverpassReader() + reader.parse_json_file(input_file_name[0]) + map_ = reader.map_ + min1 = np.array((map_.boundary_box[0].min_, map_.boundary_box[1].min_)) + max1 = np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_)) + else: + + boundary_box = list(map( + lambda x: float(x.replace('m', '-')), options.boundary_box.split(','))) + + full = False # Full keys getting + + if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]: + full = True + + osm_reader = OSMReader() + + for file_name in input_file_name: + if not os.path.isfile(file_name): + print("Fatal: no such file: " + file_name + ".") + sys.exit(1) + + osm_reader.parse_osm_file( + file_name, parse_ways=options.draw_ways, + parse_relations=options.draw_ways, full=full) + + map_: Map = osm_reader.map_ + + min1: np.array = np.array((boundary_box[1], boundary_box[0])) + max1: np.array = np.array((boundary_box[3], boundary_box[2])) + flinger: Flinger = Flinger(MinMax(min1, max1), options.scale) size: np.array = flinger.size @@ -362,7 +380,8 @@ def main(argv) -> None: if options.draw_ways: constructor.construct_ways() constructor.construct_relations() - constructor.construct_nodes() + if options.mode not in [AUTHOR_MODE, CREATION_TIME_MODE]: + constructor.construct_nodes() painter: Painter = Painter( show_missing_tags=options.show_missing_tags, overlap=options.overlap, diff --git a/roentgen/point.py b/roentgen/point.py index 7142a90..8723b89 100644 --- a/roentgen/point.py +++ b/roentgen/point.py @@ -26,10 +26,11 @@ class TextStruct: class Occupied: - def __init__(self, width: int, height: int): + def __init__(self, width: int, height: int, overlap: float): self.matrix = np.full((int(width), int(height)), False, dtype=bool) self.width = width self.height = height + self.overlap = overlap def check(self, point) -> bool: if 0 <= point[0] < self.width and 0 <= point[1] < self.height: @@ -171,13 +172,15 @@ class Point(Tagged): return is_place_for_extra: bool = True - left: float = -(len(self.icon_set.extra_icons) - 1) * 8 - for shape_ids in self.icon_set.extra_icons: - if occupied.check( - (int(self.point[0] + left), int(self.point[1] + self.y))): - is_place_for_extra = False - break - left += 16 + if occupied: + left: float = -(len(self.icon_set.extra_icons) - 1) * 8 + for shape_ids in self.icon_set.extra_icons: + if occupied.check( + (int(self.point[0] + left), + int(self.point[1] + self.y))): + is_place_for_extra = False + break + left += 16 if is_place_for_extra: left: float = -(len(self.icon_set.extra_icons) - 1) * 8 @@ -197,7 +200,7 @@ class Point(Tagged): # Down-cast floats to integers to make icons pixel-perfect. position = list(map(int, position)) - if occupied.check(position): + if occupied and occupied.check(position): return False # Draw outlines. @@ -213,9 +216,11 @@ class Point(Tagged): for icon in icons: # type: Icon icon.draw(svg, position, fill, tags=tags) - for i in range(-12, 12): - for j in range(-12, 12): - occupied.register((position[0] + i, position[1] + j)) + if occupied: + overlap: int = occupied.overlap + for i in range(-overlap, overlap): + for j in range(-overlap, overlap): + occupied.register((position[0] + i, position[1] + j)) return True @@ -240,7 +245,7 @@ class Point(Tagged): def draw_text( self, svg: svgwrite.Drawing, text: str, point, occupied: Occupied, fill: Color, size: float = 10.0, out_fill=Color("white"), - out_opacity=1.0, out_fill_2: Optional[Color] = None, + out_opacity=0.5, out_fill_2: Optional[Color] = None, out_opacity_2=1.0): """ Drawing text. @@ -253,19 +258,20 @@ class Point(Tagged): """ length = len(text) * 6 - is_occupied: bool = False - for i in range(-int(length / 2), int(length/ 2)): - if occupied.check((int(point[0] + i), int(point[1] - 4))): - is_occupied = True - break + if occupied: + is_occupied: bool = False + for i in range(-int(length / 2), int(length/ 2)): + if occupied.check((int(point[0] + i), int(point[1] - 4))): + is_occupied = True + break - if is_occupied: - return + if is_occupied: + return - for i in range(-int(length / 2), int(length / 2)): - for j in range(-12, 5): - occupied.register((int(point[0] + i), int(point[1] + j))) - # svg.add(svg.rect((point[0] + i, point[1] + j), (1, 1))) + for i in range(-int(length / 2), int(length / 2)): + for j in range(-12, 5): + occupied.register((int(point[0] + i), int(point[1] + j))) + # svg.add(svg.rect((point[0] + i, point[1] + j), (1, 1))) if out_fill_2: svg.add(svg.text( diff --git a/roentgen/ui.py b/roentgen/ui.py index f2233b9..1e69340 100644 --- a/roentgen/ui.py +++ b/roentgen/ui.py @@ -33,8 +33,7 @@ def parse_options(args) -> argparse.Namespace: "-b", "--boundary-box", dest="boundary_box", metavar=",,,", - help="geo boundary box, use space before \"-\" for negative values", - required=True) + help="geo boundary box, use space before \"-\" for negative values") parser.add_argument( "-s", "--scale", metavar="", diff --git a/test/test_icons.py b/test/test_icons.py index da7df2a..8dd484b 100644 --- a/test/test_icons.py +++ b/test/test_icons.py @@ -2,9 +2,9 @@ Author: Sergey Vartanov (me@enzet.ru). """ -from roentgen.grid import draw_grid +from roentgen.grid import draw_all_icons -def test_icons(): +def test_icons() -> None: """ Test grid drawing. """ - draw_grid() + draw_all_icons("temp.svg")