diff --git a/roentgen/address.py b/roentgen/address.py index ff308e6..a0268d3 100644 --- a/roentgen/address.py +++ b/roentgen/address.py @@ -6,8 +6,13 @@ Author: Sergey Vartanov (me@enzet.ru). from typing import List, Any, Dict -def get_address(tags: Dict[str, Any], draw_captions_mode: str): +def get_address(tags: Dict[str, Any], draw_captions_mode: str) -> List[str]: + """ + Construct address text list from the tags. + :param tags: OSM node, way or relation tags + :param draw_captions_mode: captions mode ("all", "main", or "no") + """ address: List[str] = [] if draw_captions_mode != "main": @@ -30,3 +35,5 @@ def get_address(tags: Dict[str, Any], draw_captions_mode: str): if "addr:housenumber" in tags: address.append(tags["addr:housenumber"]) tags.pop("addr:housenumber", None) + + return address diff --git a/roentgen/color.py b/roentgen/color.py index cd09a56..ba09180 100644 --- a/roentgen/color.py +++ b/roentgen/color.py @@ -1,3 +1,8 @@ +""" +Röntgen project. Color utility. + +Author: Sergey Vartanov (me@enzet.ru) +""" from typing import Any, List from colour import Color @@ -10,9 +15,8 @@ def is_bright(color: Color) -> bool: Is color bright enough to have black outline instead of white. """ return ( - 0.2126 * color.red * 256 + - 0.7152 * color.green * 256 + - 0.0722 * color.blue * 256 > 200) + 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue + > 0.78125) def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]): @@ -30,9 +34,9 @@ def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]): 0 if bounds.max_ == bounds.min_ else (value - bounds.min_) / (bounds.max_ - bounds.min_)) coefficient = min(1.0, max(0.0, coefficient)) - m: int = int(coefficient * color_length) - color_coefficient = (coefficient - m / color_length) * color_length + index: int = int(coefficient * color_length) + color_coefficient = (coefficient - index / color_length) * color_length return Color(rgb=[ - scale[m].rgb[i] + color_coefficient * - (scale[m + 1].rgb[i] - scale[m].rgb[i]) for i in range(3)]) \ No newline at end of file + scale[index].rgb[i] + color_coefficient * + (scale[index + 1].rgb[i] - scale[index].rgb[i]) for i in range(3)]) diff --git a/roentgen/constructor.py b/roentgen/constructor.py index e59cb24..b546ade 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -3,21 +3,22 @@ Construct Röntgen nodes and ways. Author: Sergey Vartanov (me@enzet.ru). """ -import numpy as np - -from colour import Color +from dataclasses import dataclass from datetime import datetime from hashlib import sha256 from typing import Any, Dict, List, Optional, Set +from colour import Color +import numpy as np + from roentgen import ui +from roentgen.color import get_gradient_color from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID from roentgen.flinger import Flinger -from roentgen.osm_reader import Map, OSMMember, OSMRelation, OSMWay, OSMNode, \ - Tagged +from roentgen.osm_reader import ( + Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged) from roentgen.scheme import IconSet, Scheme from roentgen.util import MinMax -from roentgen.color import get_gradient_color DEBUG: bool = False TIME_COLOR_SCALE: List[Color] = [ @@ -27,31 +28,35 @@ TIME_COLOR_SCALE: List[Color] = [ def is_clockwise(polygon: List[OSMNode]) -> bool: """ - Are polygon nodes are in clockwise order. + Return true if polygon nodes are in clockwise order. + + :param polygon: list of OpenStreetMap nodes """ count: float = 0 - for index in range(len(polygon)): # type: int + for index, node in enumerate(polygon): # type: int, OSMNode next_index: int = 0 if index == len(polygon) - 1 else index + 1 count += ( - (polygon[next_index].coordinates[0] - - polygon[index].coordinates[0]) * - (polygon[next_index].coordinates[1] + - polygon[index].coordinates[1])) + (polygon[next_index].coordinates[0] - node.coordinates[0]) * + (polygon[next_index].coordinates[1] + node.coordinates[1])) return count >= 0 def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: - if is_clockwise(polygon): - return polygon - else: - return list(reversed(polygon)) + """ + Make polygon nodes clockwise. + + :param polygon: list of OpenStreetMap nodes + """ + return polygon if is_clockwise(polygon) else list(reversed(polygon)) def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: - if not is_clockwise(polygon): - return polygon - else: - return list(reversed(polygon)) + """ + Make polygon nodes counter-clockwise. + + :param polygon: list of OpenStreetMap nodes + """ + return polygon if not is_clockwise(polygon) else list(reversed(polygon)) class Point(Tagged): @@ -74,11 +79,6 @@ class Point(Tagged): self.layer: float = 0 self.is_for_node: bool = is_for_node - def get_tag(self, key: str): - if key in self.tags: - return self.tags[key] - return None - class Figure(Tagged): """ @@ -107,6 +107,7 @@ class Figure(Tagged): """ Get SVG path commands. + :param flinger: convertor for geo coordinates :param shift: shift vector """ path: str = "" @@ -121,6 +122,9 @@ class Figure(Tagged): class Segment: + """ + Line segment. + """ def __init__(self, point_1: np.array, point_2: np.array): self.point_1 = point_1 self.point_2 = point_2 @@ -128,7 +132,7 @@ class Segment: difference: np.array = point_2 - point_1 vector: np.array = difference / np.linalg.norm(difference) self.angle: float = ( - np.arccos(np.dot(vector, np.array((0, 1)))) / np.pi) + np.arccos(np.dot(vector, np.array((0, 1)))) / np.pi) def __lt__(self, other: "Segment"): return (((self.point_1 + self.point_2) / 2)[1] < @@ -142,6 +146,7 @@ class Building(Figure): def __init__( self, tags: Dict[str, str], inners, outers, flinger: Flinger, style: Dict[str, Any], layer: float): + super().__init__(tags, inners, outers, style, layer) self.parts = [] @@ -154,23 +159,24 @@ class Building(Figure): self.parts = sorted(self.parts) - - def get_levels(self): + def get_levels(self) -> float: + """ + Get building level number. + """ try: - return max(3, float(self.get_tag("building:levels"))) + return max(3.0, float(self.get_tag("building:levels"))) except (ValueError, TypeError): return 3 +@dataclass class TextStruct: """ Some label on the map with attributes. """ - def __init__( - self, text: str, fill: Color = Color("#444444"), size: float = 10): - self.text = text - self.fill = fill - self.size = size + text: str + fill: Color = Color("#444444") + size: float = 10 def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array: @@ -202,6 +208,9 @@ def get_user_color(text: str, seed: str) -> Color: def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color: """ Generate color based on time. + + :param time: current element creation time + :param boundaries: minimum and maximum element creation time on the map """ return get_gradient_color(time, boundaries, TIME_COLOR_SCALE) @@ -244,11 +253,11 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]: def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str: """ - Construct SVG path from nodes. + Construct SVG path commands from nodes. """ - path = "" - prev_node = None - for node in nodes: + path: str = "" + prev_node: Optional[OSMNode] = None + for node in nodes: # type: OSMNode flung = flinger.fling(node.coordinates) + shift path += ("L" if prev_node else "M") + f" {flung[0]},{flung[1]} " prev_node = node @@ -280,11 +289,14 @@ class Constructor: self.levels: Set[float] = {0.5, 1} - def add_building(self, building: Building): + def add_building(self, building: Building) -> None: + """ + Add building and update levels. + """ self.buildings.append(building) self.levels.add(building.get_levels()) - def construct_ways(self): + def construct_ways(self) -> None: """ Construct Röntgen ways. """ @@ -303,33 +315,22 @@ class Constructor: def construct_way( self, way: Optional[OSMWay], tags: Dict[str, Any], - inners, outers) -> None: + inners: List[List[OSMNode]], outers: List[List[OSMNode]]) -> None: """ Way construction. :param way: OSM way :param tags: way tag dictionary + :param inners: list of polygons that compose inner boundary + :param outers: list of polygons that compose outer boundary """ layer: float = 0 - # level: float = 0 - # - # if "layer" in tags: - # layer = get_float(tags["layer"]) - # if "level" in tags: - # try: - # levels = list(map(float, tags["level"].split(";"))) - # level = sum(levels) / len(levels) - # except ValueError: - # pass - - # layer = 100 * level + 0.01 * layer center_point, center_coordinates = None, None - if way: - center_point, center_coordinates = \ - line_center(way.nodes, self.flinger) - nodes = way.nodes + if way is not None: + center_point, center_coordinates = ( + line_center(way.nodes, self.flinger)) if self.mode == "user-coloring": if not way: @@ -417,8 +418,8 @@ class Constructor: "stroke-width": 1} self.figures.append(Figure( tags, inners, outers, style, layer)) - if center_point is not None and (way.is_cycle() or - "area" in tags and tags["area"]): + if (center_point is not None and + way.is_cycle() and "area" in tags and tags["area"]): icon_set: IconSet = self.scheme.get_icon(tags) self.nodes.append(Point( icon_set, tags, center_point, center_coordinates, @@ -480,7 +481,7 @@ class Constructor: if self.mode == "user-coloring": icon_set.color = get_user_color(node.user, self.seed) if self.mode == "time": - icon_set.color = get_time_color(node.timestamp) + icon_set.color = get_time_color(node.timestamp, self.map_.time) self.nodes.append(Point(icon_set, tags, flung, node.coordinates)) diff --git a/roentgen/direction.py b/roentgen/direction.py index 002d6d5..59dce56 100644 --- a/roentgen/direction.py +++ b/roentgen/direction.py @@ -43,7 +43,7 @@ def parse_vector(text: str) -> Optional[np.array]: return None -def rotation_matrix(angle): +def rotation_matrix(angle) -> np.array: """ Get a matrix to rotate 2D vector by the angle. @@ -100,7 +100,7 @@ class Sector: return ["L", start, "A", radius, radius, 0, "0", 0, end] - def __str__(self): + def __str__(self) -> str: return f"{self.start}-{self.end}" @@ -115,7 +115,7 @@ class DirectionSet: """ self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";")) - def __str__(self): + def __str__(self) -> str: return ", ".join(map(str, self.sectors)) def draw(self, center: np.array, radius: float) -> Iterator[List[Path]]: diff --git a/roentgen/extract_icon.py b/roentgen/extract_icon.py index 9992cd8..50d4f8c 100644 --- a/roentgen/extract_icon.py +++ b/roentgen/extract_icon.py @@ -6,6 +6,7 @@ Author: Sergey Vartanov (me@enzet.ru). import re import xml.dom.minidom from typing import Dict +from xml.dom.minidom import Element, Node import numpy as np from svgwrite import Drawing @@ -14,6 +15,7 @@ from roentgen import ui DEFAULT_SHAPE_ID: str = "default" DEFAULT_SMALL_SHAPE_ID: str = "default_small" +STANDARD_INKSCAPE_ID: str = "(path|rect)\\d*" GRID_STEP: int = 16 @@ -71,25 +73,26 @@ class IconExtractor: content = xml.dom.minidom.parse(input_file) for element in content.childNodes: if element.nodeName == "svg": - for node in element.childNodes: - if node.nodeName in ["g", "path"]: + for node in element.childNodes: # type: Node + if isinstance(node, Element): self.parse(node) - def parse(self, node) -> None: + def parse(self, node: Element) -> None: """ Extract icon paths into a map. :param node: XML node that contains icon """ - if node.nodeName != "path": + if node.nodeName == "g": for sub_node in node.childNodes: - self.parse(sub_node) + if isinstance(sub_node, Element): + self.parse(sub_node) return - if "id" in node.attributes.keys() and \ - "d" in node.attributes.keys() and \ - node.attributes["id"].value: - path = node.attributes["d"].value + if ("id" in node.attributes.keys() and + "d" in node.attributes.keys() and + node.attributes["id"].value): + path: str = node.attributes["d"].value matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path) if not matcher: ui.error(f"invalid path: {path}") @@ -104,7 +107,9 @@ class IconExtractor: get_offset(float(matcher.group(2))))) id_: str = node.attributes["id"].value - self.icons[id_] = Icon(node.attributes["d"].value, point, id_) + matcher = re.match(STANDARD_INKSCAPE_ID, id_) + if not matcher: + self.icons[id_] = Icon(node.attributes["d"].value, point, id_) def get_path(self, id_: str) -> (Icon, bool): """ diff --git a/roentgen/flinger.py b/roentgen/flinger.py index 5a84859..dab4877 100644 --- a/roentgen/flinger.py +++ b/roentgen/flinger.py @@ -37,15 +37,15 @@ class Flinger: """ Convert geo coordinates into SVG position points. """ - def __init__(self, geo_boundaries: MinMax, scale: float = 1000): + def __init__(self, geo_boundaries: MinMax, scale: float = 18): """ :param geo_boundaries: minimum and maximum latitude and longitude :param scale: OSM zoom level """ self.geo_boundaries: MinMax = geo_boundaries self.ratio: float = ( - osm_zoom_level_to_pixels_per_meter(scale) * - EQUATOR_LENGTH / 360) + osm_zoom_level_to_pixels_per_meter(scale) * + EQUATOR_LENGTH / 360) self.size: np.array = self.ratio * ( pseudo_mercator(self.geo_boundaries.max_) - pseudo_mercator(self.geo_boundaries.min_)) diff --git a/roentgen/grid.py b/roentgen/grid.py index b35cd25..afafe58 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -5,11 +5,10 @@ Author: Sergey Vartanov (me@enzet.ru). """ import numpy as np from svgwrite import Drawing -import yaml +from typing import List, Dict, Any, Set from roentgen.extract_icon import Icon, IconExtractor - -from typing import List +from roentgen.scheme import Scheme def draw_grid(step: float = 24, columns: int = 16): @@ -19,19 +18,19 @@ def draw_grid(step: float = 24, columns: int = 16): :param step: horizontal and vertical distance between icons :param columns: the number of columns in grid """ - tags_file_name = "data/tags.yml" + tags_file_name: str = "data/tags.yml" - scheme = yaml.load(open(tags_file_name), Loader=yaml.FullLoader) + scheme: Scheme = Scheme(tags_file_name) - icons_file_name = "icons/icons.svg" - icon_grid_file_name = "icon_grid.svg" + 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 = [] + to_draw: List[Set[str]] = [] - for element in scheme["nodes"]: + for element in scheme.nodes: # type: Dict[str, Any] if "icon" in element: if set(element["icon"]) not in to_draw: to_draw.append(set(element["icon"])) @@ -41,23 +40,26 @@ def draw_grid(step: float = 24, columns: int = 16): if "over_icon" not in element: continue if "under_icon" in element: - for icon in element["under_icon"]: - current_set = set([icon] + element["over_icon"]) + for icon_id in element["under_icon"]: # type: str + current_set = set([icon_id] + element["over_icon"]) if current_set not in to_draw: to_draw.append(current_set) if not ("under_icon" in element and "with_icon" in element): continue - for icon in element["under_icon"]: - for icon2 in element["with_icon"]: - current_set = set([icon] + [icon2] + element["over_icon"]) + for icon_id in element["under_icon"]: # type: str + for icon_2_id in element["with_icon"]: # type: str + current_set: Set[str] = set( + [icon_id] + [icon_2_id] + element["over_icon"]) if current_set not in to_draw: to_draw.append(current_set) - for icon2 in element["with_icon"]: - for icon3 in element["with_icon"]: - current_set = \ - set([icon] + [icon2] + [icon3] + element["over_icon"]) - if icon2 != icon3 and icon2 != icon and icon3 != icon and \ - (current_set not in to_draw): + for icon_2_id in element["with_icon"]: # type: str + for icon_3_id in element["with_icon"]: # type: str + current_set = set( + [icon_id] + [icon_2_id] + [icon_3_id] + + 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)): to_draw.append(current_set) number: int = 0 @@ -70,8 +72,8 @@ def draw_grid(step: float = 24, columns: int = 16): found: bool = False icon_set: List[Icon] = [] for icon_id in icons_to_draw: # type: str - icon, got = extractor.get_path(icon_id) - assert got + 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 found: @@ -84,13 +86,13 @@ def draw_grid(step: float = 24, columns: int = 16): svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF")) - for icon in icons: + for combined_icon in icons: # type: List[Icon] background_color, foreground_color = "#FFFFFF", "#444444" svg.add(svg.rect( point - np.array((-10, -10)), (20, 20), fill=background_color)) - for i in icon: # type: Icon - path = i.get_path(svg, point) + for icon in combined_icon: # type: Icon + path = icon.get_path(svg, point) path.update({"fill": foreground_color}) svg.add(path) point += np.array((step, 0)) diff --git a/roentgen/mapper.py b/roentgen/mapper.py index 15ee508..a90838b 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -3,6 +3,8 @@ Simple OpenStreetMap renderer. Author: Sergey Vartanov (me@enzet.ru). """ +import argparse + import numpy as np import os import svgwrite @@ -17,7 +19,8 @@ from typing import Any, Dict, List, Optional from roentgen import ui from roentgen.address import get_address -from roentgen.constructor import Constructor, Point, Figure, TextStruct, Building +from roentgen.constructor import ( + Constructor, Point, Figure, TextStruct, Building, Segment) from roentgen.flinger import Flinger from roentgen.grid import draw_grid from roentgen.extract_icon import Icon, IconExtractor @@ -150,7 +153,7 @@ class Painter: text, point, font_size=size, text_anchor="middle", font_family=DEFAULT_FONT, fill=fill.hex)) - def construct_text(self, tags, processed): + def construct_text(self, tags, processed) -> List[TextStruct]: """ Construct labels for not processed tags. """ @@ -185,7 +188,7 @@ class Painter: alt_name = "" alt_name += "бывш. " + tags["old_name"] - address = get_address(tags, self.draw_captions) + address: List[str] = get_address(tags, self.draw_captions) if name: texts.append(TextStruct(name, Color("black"))) @@ -233,11 +236,11 @@ class Painter: ways_length: int = len(ways) for index, way in enumerate(ways): # type: Figure ui.progress_bar(index, ways_length, step=10, text="Drawing ways") - path: str = way.get_path(self.flinger) - if path: - p = Path(d=path) - p.update(way.style) - self.svg.add(p) + path_commands: str = way.get_path(self.flinger) + if path_commands: + path = Path(d=path_commands) + path.update(way.style) + self.svg.add(path) ui.progress_bar(-1, 0, text="Drawing ways") # Draw building shade. @@ -262,15 +265,18 @@ class Painter: previous_level: float = 0 height: float = self.flinger.get_scale() + level_count: int = len(constructor.levels) - for level in sorted(constructor.levels): + for index, level in enumerate(sorted(constructor.levels)): + ui.progress_bar( + index, level_count, step=1, text="Drawing buildings") fill: Color() for way in constructor.buildings: # type: Building if way.get_levels() < level: continue shift_1 = [0, -previous_level * height] shift_2 = [0, -level * height] - for segment in way.parts: + for segment in way.parts: # type: Segment if level == 0.5: fill = Color("#AAAAAA") elif level == 1: @@ -280,26 +286,29 @@ class Painter: fill = Color(rgb=(color_part, color_part, color_part)) self.svg.add(self.svg.path( - d=("M", np.add(segment.point_1, shift_1), "L", - np.add(segment.point_2, shift_1), - np.add(segment.point_2, shift_2), - np.add(segment.point_1, shift_2), - np.add(segment.point_1, shift_1), "Z"), + d=("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"), fill=fill.hex, stroke=fill.hex, stroke_width=1, stroke_linejoin="round")) - # Draw building roof. + # Draw building roofs. + for way in constructor.buildings: # type: Building if way.get_levels() == level: shift = np.array([0, -way.get_levels() * height]) - path: str = way.get_path(self.flinger, shift) - p = Path(d=path, opacity=1) - p.update(way.style) - p.update({"stroke-linejoin": "round"}) - self.svg.add(p) + path_commands: str = way.get_path(self.flinger, shift) + path = Path(d=path_commands, opacity=1) + path.update(way.style) + path.update({"stroke-linejoin": "round"}) + self.svg.add(path) previous_level = level + ui.progress_bar(-1, level_count, step=1, text="Drawing buildings") + # Trees for node in constructor.nodes: @@ -412,7 +421,7 @@ class Painter: self, icon: Icon, point: (float, float), fill: Color, tags: Dict[str, str] = None) -> None: - point = np.array(list(map(lambda x: int(x), point))) + point = np.array(list(map(int, point))) title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags)) path: svgwrite.path.Path = icon.get_path(self.svg, point) @@ -423,7 +432,7 @@ class Painter: def draw_point_outline( self, icon: Icon, point, fill: Color, mode="default"): - point = np.array(list(map(lambda x: int(x), point))) + point = np.array(list(map(int, point))) opacity: float = 0.5 stroke_width: float = 2.2 @@ -479,13 +488,13 @@ def check_level_overground(tags: Dict[str, Any]): return True -def main(argv): +def main(argv) -> None: if len(argv) == 2: if argv[1] == "grid": draw_grid() return - options = ui.parse_options(argv) + options: argparse.Namespace = ui.parse_options(argv) if not options: sys.exit(1) @@ -570,9 +579,6 @@ def main(argv): scheme=scheme) painter.draw(constructor, points) - if options.show_index: - draw_index(flinger, map_, max1, min1, svg) - print("Writing output SVG...") svg.write(open(options.output_file_name, "w")) print("Done.") @@ -584,48 +590,3 @@ def main(argv): missing_tags_file.write( f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n') missing_tags_file.close() - - -def draw_index(flinger, map_, max1, min1, svg): - print(min1[1], max1[1]) - print(min1[0], max1[0]) - lon_step = 0.001 - lat_step = 0.001 - matrix = [] - lat_number = int((max1[0] - min1[0]) / lat_step) + 1 - lon_number = int((max1[1] - min1[1]) / lon_step) + 1 - for i in range(lat_number): - row = [] - for j in range(lon_number): - row.append(0) - matrix.append(row) - for node_id in map_.node_map: # type: int - node = map_.node_map[node_id] - i = int((node[0] - min1[0]) / lat_step) - j = int((node[1] - min1[1]) / lon_step) - if (0 <= i < lat_number) and (0 <= j < lon_number): - matrix[i][j] += 1 - if "tags" in node: - matrix[i][j] += len(node.nodes) - for way_id in map_.way_map: # type: int - way = map_.way_map[way_id] - if "tags" in way: - for node_id in way.nodes: - node = map_.node_map[node_id] - i = int((node[0] - min1[0]) / lat_step) - j = int((node[1] - min1[1]) / lon_step) - if (0 <= i < lat_number) and (0 <= j < lon_number): - matrix[i][j] += len(way.nodes) / float( - len(way.nodes)) - for i in range(lat_number): - for j in range(lon_number): - t1 = flinger.fling(np.array(( - min1[0] + i * lat_step, min1[1] + j * lon_step))) - t2 = flinger.fling(np.array(( - min1[0] + (i + 1) * lat_step, - min1[1] + (j + 1) * lon_step))) - svg.add(Text( - str(int(matrix[i][j])), - (((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40), - font_size=80, fill="440000", - opacity=0.1, align="center")) diff --git a/roentgen/osm_getter.py b/roentgen/osm_getter.py index d80fe65..32a44ba 100644 --- a/roentgen/osm_getter.py +++ b/roentgen/osm_getter.py @@ -6,11 +6,11 @@ Author: Sergey Vartanov (me@enzet.ru). import os import re import time +from typing import Dict, Optional + import urllib import urllib3 -from typing import Dict, Optional - from roentgen.ui import error @@ -26,12 +26,14 @@ def get_osm(boundary_box: str, to_update: bool = False) -> Optional[str]: if not to_update and os.path.isfile(result_file_name): return open(result_file_name).read() - matcher = re.match("(?P[0-9.-]*),(?P[0-9.-]*)," + - "(?P[0-9.-]*),(?P[0-9.-]*)", boundary_box) + matcher = re.match( + "(?P[0-9.-]*),(?P[0-9.-]*)," + + "(?P[0-9.-]*),(?P[0-9.-]*)", + boundary_box) if not matcher: error("invalid boundary box") - return + return None try: left = float(matcher.group("left")) @@ -40,20 +42,21 @@ def get_osm(boundary_box: str, to_update: bool = False) -> Optional[str]: top = float(matcher.group("top")) except ValueError: error("parsing boundary box") - return + return None if left >= right: error("negative horizontal boundary") - return + return None if bottom >= top: error("negative vertical boundary") - return + return None if right - left > 0.5 or top - bottom > 0.5: error("box too big") - return + return None - content = get_data("api.openstreetmap.org/api/0.6/map", - {"bbox": boundary_box}, is_secure=True) + content = get_data( + "api.openstreetmap.org/api/0.6/map", + {"bbox": boundary_box}, is_secure=True) open(result_file_name, "w+").write(content.decode("utf-8")) diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py index 926f2cf..44723f6 100644 --- a/roentgen/osm_reader.py +++ b/roentgen/osm_reader.py @@ -3,6 +3,8 @@ Reading OpenStreetMap data from XML file. Author: Sergey Vartanov (me@enzet.ru). """ +from dataclasses import dataclass + import numpy as np from datetime import datetime from typing import Dict, List, Optional, Set, Union @@ -115,20 +117,21 @@ class OSMWay(Tagged): """ return self.nodes[0] == self.nodes[-1] - def try_to_glue(self, other: "OSMWay"): + def try_to_glue(self, other: "OSMWay") -> Optional["OSMWay"]: """ Create new combined way if ways share endpoints. """ if self.nodes[0] == other.nodes[0]: return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes) - elif self.nodes[0] == other.nodes[-1]: + if self.nodes[0] == other.nodes[-1]: return OSMWay(nodes=other.nodes[:-1] + self.nodes) - elif self.nodes[-1] == other.nodes[-1]: + if self.nodes[-1] == other.nodes[-1]: return OSMWay(nodes=self.nodes + list(reversed(other.nodes[:-1]))) - elif self.nodes[-1] == other.nodes[0]: + if self.nodes[-1] == other.nodes[0]: return OSMWay(nodes=self.nodes + other.nodes[1:]) + return None - def __repr__(self): + def __repr__(self) -> str: return f"Way <{self.id_}> {self.nodes}" @@ -175,6 +178,8 @@ def get_value(key: str, text: str): value = text[end_index:text.find('"', end_index)] return value + return None + class Map: """ @@ -243,7 +248,7 @@ class OSMReader: line = line.strip() line_number += 1 - progress_bar(line_number, lines_number) + progress_bar(line_number, lines_number, text="Parsing") # Node parsing. @@ -251,8 +256,7 @@ class OSMReader: if not parse_nodes: if parse_ways or parse_relations: continue - else: - break + break if line[-2] == "/": node: OSMNode = OSMNode().parse_from_xml(line, full) self.map_.add_node(node) @@ -267,8 +271,7 @@ class OSMReader: if not parse_ways: if parse_relations: continue - else: - break + break if line[-2] == "/": way = OSMWay().parse_from_xml(line, full) self.map_.add_way(way) @@ -293,15 +296,15 @@ class OSMReader: # Elements parsing. elif line.startswith(" argparse.Namespace: """ Parse Röntgen command-line options. """ @@ -70,14 +70,6 @@ def parse_options(args): dest="overlap", default=12, type=int) - parser.add_argument( - "--show-index", - dest="show_index", - action="store_true") - parser.add_argument( - "--no-show-index", - dest="show_index", - action="store_false") parser.add_argument( "--mode", default="normal") @@ -88,7 +80,7 @@ def parse_options(args): "--level", default=None) - arguments = parser.parse_args(args[1:]) + arguments: argparse.Namespace = parser.parse_args(args[1:]) if arguments.boundary_box: arguments.boundary_box = arguments.boundary_box.replace(" ", "") diff --git a/roentgen/util.py b/roentgen/util.py index 65cea9b..1aa9672 100644 --- a/roentgen/util.py +++ b/roentgen/util.py @@ -1,25 +1,34 @@ +""" +Röntgen utility file. + +Author: Sergey Vartanov (me@enzet.ru). +""" +from dataclasses import dataclass +from typing import Any + + +@dataclass class MinMax: """ Minimum and maximum. """ - def __init__(self, min_=None, max_=None): - self.min_ = min_ - self.max_ = max_ + min_: Any = None + max_: Any = None - def update(self, value): + def update(self, value: Any) -> None: """ Update minimum and maximum with new value. """ self.min_ = value if not self.min_ or value < self.min_ else self.min_ self.max_ = value if not self.max_ or value > self.max_ else self.max_ - def delta(self): + def delta(self) -> Any: """ Difference between maximum and minimum. """ return self.max_ - self.min_ - def center(self): + def center(self) -> Any: """ Get middle point between minimum and maximum. """ diff --git a/test/test_direction.py b/test/test_direction.py index a084162..dce9c26 100644 --- a/test/test_direction.py +++ b/test/test_direction.py @@ -9,22 +9,27 @@ from roentgen.direction import parse_vector def test_compass_points_1(): + """ Test north direction. """ assert np.allclose(parse_vector("N"), np.array([0, -1])) def test_compass_points_2(): + """ Test north-west direction. """ root: np.float64 = -np.sqrt(2) / 2 assert np.allclose(parse_vector("NW"), np.array([root, root])) def test_compass_points_3(): + """ Test south-south-west direction. """ assert np.allclose( parse_vector("SSW"), np.array([-0.38268343, 0.92387953])) -def test_wrong(): +def test_invalid(): + """ Test invalid direction representation string. """ assert not parse_vector("O") def test_degree(): + """ Test east direction. """ assert np.allclose(parse_vector("90"), np.array([1, 0])) diff --git a/test/test_icons.py b/test/test_icons.py index 2aa7ab4..da7df2a 100644 --- a/test/test_icons.py +++ b/test/test_icons.py @@ -6,4 +6,5 @@ from roentgen.grid import draw_grid def test_icons(): + """ Test grid drawing. """ draw_grid() diff --git a/test_main.py b/test_main.py index 3a7c3c0..e6ef5c8 100644 --- a/test_main.py +++ b/test_main.py @@ -1,3 +1,2 @@ def test_main(): assert True -