diff --git a/.gitignore b/.gitignore index 023cbf0..df50e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,10 @@ # Generated files -*.png *.pyc -*.svg +doc/*.html +doc/*.svg +doc/*.wiki missed_tags.yml # Test scheme files diff --git a/doc/grid.png b/doc/grid.png index 00b9679..127e4d1 100644 Binary files a/doc/grid.png and b/doc/grid.png differ diff --git a/doc/surveillance.png b/doc/surveillance.png index 0b76815..81b8eec 100644 Binary files a/doc/surveillance.png and b/doc/surveillance.png differ diff --git a/doc/time.png b/doc/time.png index 8046d46..c16f82b 100644 Binary files a/doc/time.png and b/doc/time.png differ diff --git a/doc/trees.png b/doc/trees.png index a09d427..d5ab2cf 100644 Binary files a/doc/trees.png and b/doc/trees.png differ diff --git a/doc/user.png b/doc/user.png index 73dd06c..a238173 100644 Binary files a/doc/user.png and b/doc/user.png differ diff --git a/doc/viewpoints.png b/doc/viewpoints.png index eb7f3f8..d04a011 100644 Binary files a/doc/viewpoints.png and b/doc/viewpoints.png differ diff --git a/roentgen/constructor.py b/roentgen/constructor.py index 16de3ca..46d803a 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -249,8 +249,9 @@ class Constructor: Röntgen node and way constructor. """ def __init__( - self, check_level, mode: str, seed: str, map_: Map, - flinger: Flinger, scheme: Scheme, icon_extractor: IconExtractor): + self, map_: Map, flinger: Flinger, scheme: Scheme, + icon_extractor: IconExtractor, check_level=lambda x: True, + mode: str = "normal", seed: str = ""): self.check_level = check_level self.mode: str = mode @@ -273,6 +274,14 @@ class Constructor: self.buildings.append(building) self.levels.add(building.get_levels()) + def construct(self) -> None: + """ + Construct nodes, ways, and relations. + """ + self.construct_ways() + self.construct_relations() + self.construct_nodes() + def construct_ways(self) -> None: """ Construct Röntgen ways. diff --git a/roentgen/grid.py b/roentgen/grid.py index bd0d4b9..d46b6f6 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -7,16 +7,22 @@ import numpy as np from colour import Color from svgwrite import Drawing from typing import List, Dict, Any, Set +from os.path import join from roentgen.icon import Icon, IconExtractor from roentgen.scheme import Scheme -def draw_all_icons(output_file_name: str, columns: int = 16, step: float = 24): +def draw_all_icons( + output_file_name: str, output_directory: str, columns: int = 16, + step: float = 24 + ) -> None: """ Draw all possible icon combinations in grid. :param output_file_name: output SVG file name for icon grid + :param output_directory: path to the directory to store individual SVG files + for icons :param columns: the number of columns in grid :param step: horizontal and vertical distance between icons """ @@ -66,13 +72,16 @@ def draw_all_icons(output_file_name: str, columns: int = 16, step: float = 24): "Icons with no tag specification: \n " + ", ".join(sorted(extractor.icons.keys() - specified_ids)) + ".") - draw_grid(output_file_name, to_draw, extractor, columns, step) + draw_grid( + output_file_name, to_draw, extractor, output_directory, 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")): + extractor: IconExtractor, output_directory: str, columns: int = 16, + step: float = 24, color=Color("#444444") + ) -> List[List[Icon]]: """ Draw icons in the form of table @@ -80,9 +89,11 @@ def draw_grid( :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 output_directory: path to the directory to store individual SVG files + for icons :param columns: number of columns in grid :param step: horizontal and vertical distance between icons in grid - :return: + :param color: icon foreground color """ point: np.array = np.array((step / 2, step / 2)) width: float = step * columns @@ -92,14 +103,21 @@ def draw_grid( for icons_to_draw in combined_icon_ids: # type: Set[str] found: bool = False icon_set: List[Icon] = [] + names = [] for icon_id in icons_to_draw: # type: str icon, extracted = extractor.get_path(icon_id) # type: Icon, bool assert extracted, f"no icon with ID {icon_id}" icon_set.append(icon) found = True + if icon.name: + names.append(icon.name) if found: icons.append(icon_set) number += 1 + draw_icon( + join(output_directory, f"Röntgen {' + '.join(names)}.svg"), + icons_to_draw, extractor + ) height: int = int(int(number / (width / step) + 1) * step) svg: Drawing = Drawing(file_name, (width, height)) @@ -124,3 +142,26 @@ def draw_grid( with open(file_name, "w") as output_file: svg.write(output_file) + + return icons + + +def draw_icon( + file_name: str, icon_ids: Set[str], extractor: IconExtractor + ) -> None: + + icon_set: List[Icon] = [] + for icon_id in icon_ids: # type: str + icon, extracted = extractor.get_path(icon_id) # type: Icon, bool + assert extracted, f"no icon with ID {icon_id}" + icon_set.append(icon) + + svg: Drawing = Drawing(file_name, (16, 16)) + + for icon in icon_set: # type: Icon + path = icon.get_path(svg, (8, 8)) + path.update({"fill": "black"}) + svg.add(path) + + with open(file_name, "w") as output_file: + svg.write(output_file) diff --git a/roentgen/icon.py b/roentgen/icon.py index 9941338..bd39e4b 100644 --- a/roentgen/icon.py +++ b/roentgen/icon.py @@ -4,10 +4,9 @@ Extract icons from SVG file. Author: Sergey Vartanov (me@enzet.ru). """ import re -import xml.dom.minidom from dataclasses import dataclass -from typing import Dict, Any, Set -from xml.dom.minidom import Document, Element, Node +from typing import Dict, Any, Optional +from xml.dom.minidom import Document, Element, Node, parse import numpy as np import svgwrite @@ -31,6 +30,7 @@ class Icon: path: str # SVG icon path offset: np.array # vector that should be used to shift the path id_: str # shape identifier + name: Optional[str] = None # icon description def is_default(self) -> bool: """ @@ -96,7 +96,7 @@ class IconExtractor: self.icons: Dict[str, Icon] = {} with open(svg_file_name) as input_file: - content = xml.dom.minidom.parse(input_file) # type: Document + content = parse(input_file) # type: Document for element in content.childNodes: # type: Element if element.nodeName != "svg": continue @@ -116,14 +116,16 @@ class IconExtractor: self.parse(sub_node) return - if ("id" in node.attributes.keys() and - "d" in node.attributes.keys() and - node.attributes["id"].value): - path: str = node.attributes["d"].value + if (node.hasAttribute("id") and node.hasAttribute("d") and + node.getAttribute("id")): + + path: str = node.getAttribute("d") matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path) if not matcher: return + name: Optional[str] = None + def get_offset(value: float): """ Get negated icon offset from the origin. """ return -int(value / GRID_STEP) * GRID_STEP - GRID_STEP / 2 @@ -132,10 +134,15 @@ class IconExtractor: get_offset(float(matcher.group(1))), get_offset(float(matcher.group(2))))) - id_: str = node.attributes["id"].value + for child_node in node.childNodes: + if isinstance(child_node, Element): + name = child_node.childNodes[0].nodeValue + break + + id_: str = node.getAttribute("id") matcher = re.match(STANDARD_INKSCAPE_ID, id_) if not matcher: - self.icons[id_] = Icon(node.attributes["d"].value, point, id_) + self.icons[id_] = Icon(path, point, id_, name) def get_path(self, id_: str) -> (Icon, bool): """ diff --git a/roentgen/mapper.py b/roentgen/mapper.py index 1b647e8..ff8afde 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -43,14 +43,13 @@ class Painter: """ def __init__( - self, show_missing_tags: bool, overlap: int, draw_nodes: bool, - mode: str, draw_captions: str, map_: Map, flinger: Flinger, + self, map_: Map, flinger: Flinger, svg: svgwrite.Drawing, icon_extractor: IconExtractor, - scheme: Scheme): + scheme: Scheme, show_missing_tags: bool = False, overlap: int = 12, + mode: str = "normal", draw_captions: str = "main"): self.show_missing_tags: bool = show_missing_tags self.overlap: int = overlap - self.draw_nodes: bool = draw_nodes self.mode: str = mode self.draw_captions: str = draw_captions @@ -60,10 +59,17 @@ class Painter: self.icon_extractor = icon_extractor self.scheme: Scheme = scheme + self.background_color: Color = self.scheme.get_color("background_color") + if self.mode in [AUTHOR_MODE, CREATION_TIME_MODE]: + self.background_color: Color = Color("#111111") + def draw(self, constructor: Constructor): """ Draw map. """ + self.svg.add(Rect( + (0, 0), self.flinger.size, fill=self.background_color)) + ways = sorted(constructor.figures, key=lambda x: x.line_style.priority) ways_length: int = len(ways) for index, way in enumerate(ways): # type: Figure @@ -179,19 +185,16 @@ class Painter: angle = float(node.get_tag("camera:angle")) if "angle" in node.tags: angle = float(node.get_tag("angle")) - direction_radius: float = ( - 25 * self.flinger.get_scale(node.coordinates)) + direction_radius: float = (25) direction_color: Color = ( self.scheme.get_color("direction_camera_color")) elif node.get_tag("traffic_sign") == "stop": direction = node.get_tag("direction") - direction_radius: float = ( - 25 * self.flinger.get_scale(node.coordinates)) + direction_radius: float = (25) direction_color: Color = Color("red") else: direction = node.get_tag("direction") - direction_radius: float = ( - 50 * self.flinger.get_scale(node.coordinates)) + direction_radius: float = (50) direction_color: Color = ( self.scheme.get_color("direction_view_color")) is_revert_gradient = True @@ -238,15 +241,11 @@ class Painter: continue ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes") node.draw_shapes(self.svg, occupied) - ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes") - - if self.draw_captions == "no": - return - - for node in nodes: # type: Point - if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]: + if (self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE] and + self.draw_captions != "no"): node.draw_texts( self.svg, self.scheme, occupied, self.draw_captions) + ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes") def check_level_number(tags: Dict[str, Any], level: float): @@ -295,7 +294,8 @@ def main(argv) -> None: """ if len(argv) == 2: if argv[1] == "grid": - draw_all_icons("icon_grid.svg") + os.makedirs("icon_set", exist_ok=True) + draw_all_icons("icon_grid.svg", "icon_set") return options: argparse.Namespace = ui.parse_options(argv) @@ -303,10 +303,6 @@ def main(argv) -> None: if not options: sys.exit(1) - background_color: Color = Color("#EEEEEE") - if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]: - background_color: Color = Color("#111111") - if options.input_file_name: input_file_name = options.input_file_name else: @@ -325,8 +321,7 @@ def main(argv) -> None: 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(','))) + boundary_box = list(map(float, options.boundary_box.split(','))) full = False # Full keys getting @@ -340,9 +335,7 @@ def main(argv) -> None: 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) + osm_reader.parse_osm_file(file_name, full=full) map_: Map = osm_reader.map_ @@ -354,7 +347,6 @@ def main(argv) -> None: svg: svgwrite.Drawing = ( svgwrite.Drawing(options.output_file_name, size=size)) - svg.add(Rect((0, 0), size, fill=background_color)) icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME) @@ -375,18 +367,13 @@ def main(argv) -> None: return not check_level_number(x, float(options.level)) constructor: Constructor = Constructor( - check_level, options.mode, options.seed, map_, flinger, scheme, - icon_extractor) - if options.draw_ways: - constructor.construct_ways() - constructor.construct_relations() - if options.mode not in [AUTHOR_MODE, CREATION_TIME_MODE]: - constructor.construct_nodes() + map_, flinger, scheme, icon_extractor, check_level, options.mode, + options.seed) + constructor.construct() painter: Painter = Painter( show_missing_tags=options.show_missing_tags, overlap=options.overlap, - draw_nodes=options.draw_nodes, mode=options.mode, - draw_captions=options.draw_captions, + mode=options.mode, draw_captions=options.draw_captions, map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor, scheme=scheme) diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py index 8298735..e175821 100644 --- a/roentgen/osm_reader.py +++ b/roentgen/osm_reader.py @@ -77,7 +77,8 @@ class OSMNode(Tagged): self.id_ = structure["id"] self.coordinates = np.array((structure["lat"], structure["lon"])) - self.tags = structure["tags"] + if "tags" in structure: + self.tags = structure["tags"] return self @@ -119,6 +120,16 @@ class OSMWay(Tagged): return self + def parse_from_structure(self, structure: Dict[str, Any], nodes) -> "OSMWay": + + self.id_ = structure["id"] + for node_id in structure["nodes"]: + self.nodes.append(nodes[node_id]) + if "tags" in structure: + self.tags = structure["tags"] + + return self + def is_cycle(self) -> bool: """ Is way a cycle way or an area boundary. @@ -171,16 +182,37 @@ class OSMRelation(Tagged): return self + def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMRelation": + + self.id_ = structure["id"] + for member in structure["members"]: + mem = OSMMember() + mem.type_ = member["type"] + mem.role = member["role"] + mem.ref = member["ref"] + self.members.append(mem) + if "tags" in structure: + self.tags = structure["tags"] + + return self + class OSMMember: """ Member of OpenStreetMap relation. """ - def __init__(self, text: str): + def __init__(self): + self.type_ = "" + self.ref = 0 + self.role = "" + + def parse_from_xml(self, text: str) -> "OSMMember": self.type_: str = get_value("type", text) self.ref: int = int(get_value("ref", text)) self.role: str = get_value("role", text) + return self + def get_value(key: str, text: str): """ @@ -239,14 +271,29 @@ class OverpassReader: def __init__(self): self.map_ = Map() - def parse_json_file(self, file_name: str): + def parse_json_file(self, file_name: str) -> Map: with open(file_name) as input_file: structure = json.load(input_file) + node_map = {} + way_map = {} + for element in structure["elements"]: if element["type"] == "node": node = OSMNode().parse_from_structure(element) + node_map[node.id_] = node self.map_.add_node(node) + for element in structure["elements"]: + if element["type"] == "way": + way = OSMWay().parse_from_structure(element, node_map) + way_map[way.id_] = way + self.map_.add_way(way) + for element in structure["elements"]: + if element["type"] == "relation": + relation = OSMRelation().parse_from_structure(element) + self.map_.add_relation(relation) + + return self.map_ class OSMReader: @@ -334,7 +381,7 @@ class OSMReader: element.nodes.append( self.map_.node_map[int(get_value("ref", line))]) elif line.startswith(" argparse.Namespace: default=18, dest="scale", type=float) - parser.add_argument( - "-nn", "--no-draw-nodes", - dest="draw_nodes", - action="store_false", - default=True) - parser.add_argument( - "-nw", "--no-draw-ways", - dest="draw_ways", - action="store_false", - default=True) parser.add_argument( "--captions", "--no-draw-captions", dest="draw_captions", @@ -86,6 +76,26 @@ def parse_options(args) -> argparse.Namespace: return arguments +def progress_bar1( + number: int, total: int, length: int = 20, step: int = 1000) -> None: + """ + Draw progress bar using Unicode symbols. + + :param number: current value + :param total: maximum value + :param length: progress bar length. + :param step: frequency of progress bar updating (assuming that numbers go + subsequently) + :param text: short description + """ + ratio: float = number / total + parts: int = int(ratio * length * BOXES_LENGTH) + fill_length: int = int(parts / BOXES_LENGTH) + box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)] + return ( + f"{fill_length * '█'}{box}{int(length - fill_length - 1) * ' '}") + + def progress_bar( number: int, total: int, length: int = 20, step: int = 1000, text: str = "") -> None: diff --git a/roentgen/util.py b/roentgen/util.py index 1aa9672..9323cd2 100644 --- a/roentgen/util.py +++ b/roentgen/util.py @@ -33,3 +33,6 @@ class MinMax: Get middle point between minimum and maximum. """ return (self.min_ + self.max_) / 2 + + def __repr__(self) -> str: + return f"{self.min_}:{self.max_}" diff --git a/test/test_icons.py b/test/test_icons.py index 8dd484b..35cdf39 100644 --- a/test/test_icons.py +++ b/test/test_icons.py @@ -1,10 +1,12 @@ """ Author: Sergey Vartanov (me@enzet.ru). """ +from os import makedirs from roentgen.grid import draw_all_icons def test_icons() -> None: """ Test grid drawing. """ - draw_all_icons("temp.svg") + makedirs("icon_set", exist_ok=True) + draw_all_icons("temp.svg", "icon_set")