diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 321c341..16feb3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.md b/README.md index fc092a1..643c716 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Every way and node displayed with the random color picked for each author with ` Installation ------------ -Requirements: Python 3.9. +Requirements: Python 3.8. To install all packages, run: diff --git a/data/githooks/pre-commit b/data/githooks/pre-commit index 3206d43..8502617 100755 --- a/data/githooks/pre-commit +++ b/data/githooks/pre-commit @@ -13,5 +13,5 @@ echo "Lint with Flake8..." flake8 \ --max-line-length=80 \ --ignore=E203,W503,ANN002,ANN003,ANN101,ANN102 \ - --exclude=work,precommit.py,tests/test_road.py \ + --exclude=work,precommit.py,tests/test_road.py,python3.8 \ || { echo "FAIL"; exit 1; } diff --git a/doc/readme.moi b/doc/readme.moi index 7eafd44..655db0e 100644 --- a/doc/readme.moi +++ b/doc/readme.moi @@ -168,7 +168,7 @@ Every way and node displayed with the random color picked for each author with \ \2 {Installation} {installation} -Requirements\: Python 3.9/* or higher*/. +Requirements\: Python 3.8/* or higher*/. To install all packages, run\: diff --git a/map_machine/color.py b/map_machine/color.py index b6d96cd..a656052 100644 --- a/map_machine/color.py +++ b/map_machine/color.py @@ -1,7 +1,7 @@ """ Color utility. """ -from typing import Any +from typing import Any, List from colour import Color @@ -22,7 +22,7 @@ def is_bright(color: Color) -> bool: def get_gradient_color( - value: Any, bounds: MinMax, colors: list[Color] + value: Any, bounds: MinMax, colors: List[Color] ) -> Color: """ Get color from the color scale for the value. @@ -32,7 +32,7 @@ def get_gradient_color( :param colors: color scale """ color_length: int = len(colors) - 1 - scale: list[Color] = colors + [Color("black")] + scale: List[Color] = colors + [Color("black")] range_coefficient: float = ( 0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta() diff --git a/map_machine/constructor.py b/map_machine/constructor.py index 951841e..b7db07f 100644 --- a/map_machine/constructor.py +++ b/map_machine/constructor.py @@ -4,7 +4,7 @@ Construct Map Machine nodes and ways. import logging from datetime import datetime from hashlib import sha256 -from typing import Any, Iterator, Optional, Union +from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union import numpy as np from colour import Color @@ -46,7 +46,7 @@ __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" DEBUG: bool = False -TIME_COLOR_SCALE: list[Color] = [ +TIME_COLOR_SCALE: List[Color] = [ Color("#581845"), Color("#900C3F"), Color("#C70039"), @@ -57,7 +57,7 @@ TIME_COLOR_SCALE: list[Color] = [ def line_center( - nodes: list[OSMNode], flinger: Flinger + nodes: List[OSMNode], flinger: Flinger ) -> (np.ndarray, np.ndarray): """ Get geometric center of nodes set. @@ -65,7 +65,7 @@ def line_center( :param nodes: node list :param flinger: flinger that remap geo positions """ - boundary: list[MinMax] = [MinMax(), MinMax()] + boundary: List[MinMax] = [MinMax(), MinMax()] for node in nodes: boundary[0].update(node.coordinates[0]) @@ -95,14 +95,14 @@ def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color: return get_gradient_color(time, boundaries, TIME_COLOR_SCALE) -def glue(ways: list[OSMWay]) -> list[list[OSMNode]]: +def glue(ways: List[OSMWay]) -> List[List[OSMNode]]: """ Try to glue ways that share nodes. :param ways: ways to glue """ - result: list[list[OSMNode]] = [] - to_process: set[tuple[OSMNode]] = set() + result: List[List[OSMNode]] = [] + to_process: Set[Tuple[OSMNode]] = set() for way in ways: if way.is_cycle(): @@ -111,9 +111,9 @@ def glue(ways: list[OSMWay]) -> list[list[OSMNode]]: to_process.add(tuple(way.nodes)) while to_process: - nodes: list[OSMNode] = list(to_process.pop()) - glued: Optional[list[OSMNode]] = None - other_nodes: Optional[tuple[OSMNode]] = None + nodes: List[OSMNode] = list(to_process.pop()) + glued: Optional[List[OSMNode]] = None + other_nodes: Optional[Tuple[OSMNode]] = None for other_nodes in to_process: glued = try_to_glue(nodes, list(other_nodes)) @@ -132,14 +132,14 @@ def glue(ways: list[OSMWay]) -> list[list[OSMNode]]: return result -def is_cycle(nodes: list[OSMNode]) -> bool: +def is_cycle(nodes: List[OSMNode]) -> bool: """Is way a cycle way or an area boundary.""" return nodes[0] == nodes[-1] def try_to_glue( - nodes: list[OSMNode], other: list[OSMNode] -) -> Optional[list[OSMNode]]: + nodes: List[OSMNode], other: List[OSMNode] +) -> Optional[List[OSMNode]]: """Create new combined way if ways share endpoints.""" if nodes[0] == other[0]: return list(reversed(other[1:])) + nodes @@ -182,15 +182,15 @@ class Constructor: x, float(self.configuration.level) ) - self.points: list[Point] = [] - self.figures: list[StyledFigure] = [] - self.buildings: list[Building] = [] + self.points: List[Point] = [] + self.figures: List[StyledFigure] = [] + self.buildings: List[Building] = [] self.roads: Roads = Roads() - self.trees: list[Tree] = [] - self.craters: list[Crater] = [] - self.direction_sectors: list[DirectionSector] = [] + self.trees: List[Tree] = [] + self.craters: List[Crater] = [] + self.direction_sectors: List[DirectionSector] = [] - self.heights: set[float] = {2, 4} + self.heights: Set[float] = {2, 4} def add_building(self, building: Building) -> None: """Add building and update levels.""" @@ -221,8 +221,8 @@ class Constructor: def construct_line( self, line: Union[OSMWay, OSMRelation], - inners: list[list[OSMNode]], - outers: list[list[OSMNode]], + inners: List[List[OSMNode]], + outers: List[List[OSMNode]], ) -> None: """ Way or relation construction. @@ -265,7 +265,7 @@ class Constructor: ) return - processed: set[str] = set() + processed: Set[str] = set() recolor: Optional[Color] = None @@ -275,11 +275,11 @@ class Constructor: recolor = self.scheme.get_color(line.tags[color_tag_key]) processed.add(color_tag_key) - line_styles: list[LineStyle] = self.scheme.get_style(line.tags) + line_styles: List[LineStyle] = self.scheme.get_style(line.tags) for line_style in line_styles: if recolor is not None: - new_style: dict[str, Union[float, int, str]] = dict( + new_style: Dict[str, Union[float, int, str]] = dict( line_style.style ) new_style["stroke"] = recolor.hex @@ -302,7 +302,7 @@ class Constructor: self.extractor, line.tags, processed, self.configuration ) if icon_set is not None: - labels: list[Label] = self.scheme.construct_text( + labels: List[Label] = self.scheme.construct_text( line.tags, "all", processed ) point: Point = Point( @@ -319,7 +319,7 @@ class Constructor: if not line_styles: if DEBUG: - style: dict[str, Any] = { + style: Dict[str, Any] = { "fill": "none", "stroke": Color("red").hex, "stroke-width": 1, @@ -329,7 +329,7 @@ class Constructor: ) self.figures.append(figure) - processed: set[str] = set() + processed: Set[str] = set() priority: int icon_set: IconSet @@ -340,7 +340,7 @@ class Constructor: self.configuration, ) if icon_set is not None: - labels: list[Label] = self.scheme.construct_text( + labels: List[Label] = self.scheme.construct_text( line.tags, "all", processed ) point: Point = Point( @@ -358,12 +358,12 @@ class Constructor: def draw_special_mode( self, line: Union[OSMWay, OSMRelation], - inners: list[list[OSMNode]], - outers: list[list[OSMNode]], + inners: List[List[OSMNode]], + outers: List[List[OSMNode]], color: Color, ) -> None: """Add figure for special mode: time or author.""" - style: dict[str, Any] = { + style: Dict[str, Any] = { "fill": "none", "stroke": color.hex, "stroke-width": 1, @@ -376,13 +376,13 @@ class Constructor: """Construct Map Machine ways from OSM relations.""" for relation_id in self.osm_data.relations: relation: OSMRelation = self.osm_data.relations[relation_id] - tags: dict[str, str] = relation.tags + tags: Dict[str, str] = relation.tags if not self.check_level(tags): continue if "type" not in tags or tags["type"] != "multipolygon": continue - inner_ways: list[OSMWay] = [] - outer_ways: list[OSMWay] = [] + inner_ways: List[OSMWay] = [] + outer_ways: List[OSMWay] = [] for member in relation.members: if member.type_ == "way": if member.role == "inner": @@ -394,8 +394,8 @@ class Constructor: else: logging.warning(f'Unknown member role "{member.role}".') if outer_ways: - inners_path: list[list[OSMNode]] = glue(inner_ways) - outers_path: list[list[OSMNode]] = glue(outer_ways) + inners_path: List[List[OSMNode]] = glue(inner_ways) + outers_path: List[List[OSMNode]] = glue(outer_ways) self.construct_line(relation, inners_path, outers_path) def construct_nodes(self) -> None: @@ -414,11 +414,11 @@ class Constructor: def construct_node(self, node: OSMNode) -> None: """Draw one node.""" - tags: dict[str, str] = node.tags + tags: Dict[str, str] = node.tags if not self.check_level(tags): return - processed: set[str] = set() + processed: Set[str] = set() flung: np.ndarray = self.flinger.fling(node.coordinates) @@ -455,7 +455,7 @@ class Constructor: ) if icon_set is None: return - labels: list[Label] = self.scheme.construct_text(tags, "all", processed) + labels: List[Label] = self.scheme.construct_text(tags, "all", processed) self.scheme.process_ignored(tags, processed) if node.get_tag("natural") == "tree" and ( @@ -483,7 +483,7 @@ class Constructor: self.points.append(point) -def check_level_number(tags: dict[str, Any], level: float) -> bool: +def check_level_number(tags: Dict[str, Any], level: float) -> bool: """Check if element described by tags is no the specified level.""" if "level" in tags: if level not in parse_levels(tags["level"]): @@ -493,7 +493,7 @@ def check_level_number(tags: dict[str, Any], level: float) -> bool: return True -def check_level_overground(tags: dict[str, Any]) -> bool: +def check_level_overground(tags: Dict[str, Any]) -> bool: """Check if element described by tags is overground.""" if "level" in tags: try: diff --git a/map_machine/direction.py b/map_machine/direction.py index e915cf0..db73e35 100644 --- a/map_machine/direction.py +++ b/map_machine/direction.py @@ -1,7 +1,7 @@ """ Direction tag support. """ -from typing import Iterator, Optional +from typing import Iterator, List, Optional import numpy as np from portolan import middle @@ -65,7 +65,7 @@ class Sector: self.main_direction: Optional[np.ndarray] = None if "-" in text: - parts: list[str] = text.split("-") + parts: List[str] = text.split("-") self.start = parse_vector(parts[0]) self.end = parse_vector(parts[1]) self.main_direction = (self.start + self.end) / 2 @@ -152,7 +152,7 @@ class DirectionSet: :return: true if direction is right, false if direction is left, and None otherwise. """ - result: list[bool] = [x.is_right() for x in self.sectors] + result: List[bool] = [x.is_right() for x in self.sectors] if result == [True] * len(result): return True if result == [False] * len(result): diff --git a/map_machine/drawing.py b/map_machine/drawing.py index 27976d1..f4e6c09 100644 --- a/map_machine/drawing.py +++ b/map_machine/drawing.py @@ -3,7 +3,7 @@ Drawing utility. """ from dataclasses import dataclass from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union import cairo import numpy as np @@ -18,7 +18,7 @@ from svgwrite.text import Text __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -PathCommands = list[Union[float, str, np.ndarray]] +PathCommands = List[Union[float, str, np.ndarray]] @dataclass @@ -75,7 +75,7 @@ class Drawing: """Draw rectangle.""" raise NotImplementedError - def line(self, points: list[np.ndarray], style: Style) -> None: + def line(self, points: List[np.ndarray], style: Style) -> None: """Draw line.""" raise NotImplementedError @@ -117,7 +117,7 @@ class SVGDrawing(Drawing): style.update_svg_element(rectangle) self.image.add(rectangle) - def line(self, points: list[np.ndarray], style: Style) -> None: + def line(self, points: List[np.ndarray], style: Style) -> None: """Draw line.""" commands: PathCommands = ["M"] for point in points: @@ -169,7 +169,7 @@ class PNGDrawing(Drawing): self.context.rectangle(point_1[0], point_1[1], size[0], size[1]) style.draw_png_stroke(self.context) - def line(self, points: list[np.ndarray], style: Style) -> None: + def line(self, points: List[np.ndarray], style: Style) -> None: """Draw line.""" if style.fill is not None: self.context.move_to(float(points[0][0]), float(points[0][1])) @@ -283,7 +283,7 @@ class PNGDrawing(Drawing): def parse_path(path: str) -> PathCommands: """Parse path command from text representation into list.""" - parts: list[str] = path.split(" ") + parts: List[str] = path.split(" ") result: PathCommands = [] command: str = "M" index: int = 0 @@ -296,7 +296,7 @@ def parse_path(path: str) -> PathCommands: result.append(float(part)) else: if "," in part: - elements: list[str] = part.split(",") + elements: List[str] = part.split(",") result.append(np.array(list(map(float, elements)))) else: result.append(np.array((float(part), float(parts[index + 1])))) diff --git a/map_machine/element.py b/map_machine/element.py index d68d944..1ece0d4 100644 --- a/map_machine/element.py +++ b/map_machine/element.py @@ -4,6 +4,7 @@ Drawing separate map elements. import argparse import logging from pathlib import Path +from typing import Dict, List, Set import numpy as np import svgwrite @@ -33,17 +34,17 @@ def draw_element(options: argparse.Namespace) -> None: target = "area" tags_description = options.area - tags: dict[str, str] = dict( + tags: Dict[str, str] = dict( [x.split("=") for x in tags_description.split(",")] ) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) extractor: ShapeExtractor = ShapeExtractor( workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH ) - processed: set[str] = set() + processed: Set[str] = set() icon, priority = scheme.get_icon(extractor, tags, processed) is_for_node: bool = target == "node" - labels: list[Label] = scheme.construct_text(tags, "all", processed) + labels: List[Label] = scheme.construct_text(tags, "all", processed) point: Point = Point( icon, labels, diff --git a/map_machine/figure.py b/map_machine/figure.py index ef45212..67d2c78 100644 --- a/map_machine/figure.py +++ b/map_machine/figure.py @@ -1,7 +1,7 @@ """ Figures displayed on the map. """ -from typing import Any, Iterator, Optional +from typing import Any, Dict, Iterator, List, Optional import numpy as np from colour import Color @@ -31,14 +31,14 @@ class Figure(Tagged): def __init__( self, - tags: dict[str, str], - inners: list[list[OSMNode]], - outers: list[list[OSMNode]], + tags: Dict[str, str], + inners: List[List[OSMNode]], + outers: List[List[OSMNode]], ) -> None: super().__init__(tags) - self.inners: list[list[OSMNode]] = list(map(make_clockwise, inners)) - self.outers: list[list[OSMNode]] = list( + self.inners: List[List[OSMNode]] = list(map(make_clockwise, inners)) + self.outers: List[List[OSMNode]] = list( map(make_counter_clockwise, outers) ) @@ -69,20 +69,20 @@ class Building(Figure): def __init__( self, - tags: dict[str, str], - inners: list[list[OSMNode]], - outers: list[list[OSMNode]], + 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] = { + 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] = [] + self.parts: List[Segment] = [] for nodes in self.inners + self.outers: for i in range(len(nodes) - 1): @@ -198,9 +198,9 @@ class StyledFigure(Figure): def __init__( self, - tags: dict[str, str], - inners: list[list[OSMNode]], - outers: list[list[OSMNode]], + tags: Dict[str, str], + inners: List[List[OSMNode]], + outers: List[List[OSMNode]], line_style: LineStyle, ) -> None: super().__init__(tags, inners, outers) @@ -213,7 +213,7 @@ class Crater(Tagged): """ def __init__( - self, tags: dict[str, str], coordinates: np.ndarray, point: np.ndarray + self, tags: Dict[str, str], coordinates: np.ndarray, point: np.ndarray ) -> None: super().__init__(tags) self.coordinates: np.ndarray = coordinates @@ -252,7 +252,7 @@ class Tree(Tagged): """ def __init__( - self, tags: dict[str, str], coordinates: np.ndarray, point: np.ndarray + self, tags: Dict[str, str], coordinates: np.ndarray, point: np.ndarray ) -> None: super().__init__(tags) self.coordinates: np.ndarray = coordinates @@ -279,7 +279,7 @@ class DirectionSector(Tagged): Sector that represents direction. """ - def __init__(self, tags: dict[str, str], point: np.ndarray) -> None: + def __init__(self, tags: Dict[str, str], point: np.ndarray) -> None: super().__init__(tags) self.point: np.ndarray = point @@ -366,7 +366,7 @@ class Segment: ) # fmt: skip -def is_clockwise(polygon: list[OSMNode]) -> bool: +def is_clockwise(polygon: List[OSMNode]) -> bool: """ Return true if polygon nodes are in clockwise order. @@ -381,7 +381,7 @@ def is_clockwise(polygon: list[OSMNode]) -> bool: return count >= 0 -def make_clockwise(polygon: list[OSMNode]) -> list[OSMNode]: +def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: """ Make polygon nodes clockwise. @@ -390,7 +390,7 @@ def make_clockwise(polygon: list[OSMNode]) -> list[OSMNode]: return polygon if is_clockwise(polygon) else list(reversed(polygon)) -def make_counter_clockwise(polygon: list[OSMNode]) -> list[OSMNode]: +def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: """ Make polygon nodes counter-clockwise. @@ -399,7 +399,7 @@ def make_counter_clockwise(polygon: list[OSMNode]) -> list[OSMNode]: return polygon if not is_clockwise(polygon) else list(reversed(polygon)) -def get_path(nodes: list[OSMNode], shift: np.ndarray, flinger: Flinger) -> str: +def get_path(nodes: List[OSMNode], shift: np.ndarray, flinger: Flinger) -> str: """Construct SVG path commands from nodes.""" return Polyline( [flinger.fling(x.coordinates) + shift for x in nodes] diff --git a/map_machine/grid.py b/map_machine/grid.py index 57431f6..372a74f 100644 --- a/map_machine/grid.py +++ b/map_machine/grid.py @@ -4,7 +4,7 @@ Icon grid drawing. import logging from dataclasses import dataclass from pathlib import Path -from typing import Optional, Union +from typing import Dict, List, Optional, Set, Union import numpy as np from colour import Color @@ -25,7 +25,7 @@ class IconCollection: Collection of icons. """ - icons: list[Icon] + icons: List[Icon] @classmethod def from_scheme( @@ -48,11 +48,11 @@ class IconCollection: tags :param add_all: create icons from all possible shapes including parts """ - icons: list[Icon] = [] + icons: List[Icon] = [] def add() -> Icon: """Construct icon and add it to the list.""" - specifications: list[ShapeSpecification] = [ + specifications: List[ShapeSpecification] = [ scheme.get_shape_specification(x, extractor) for x in current_set ] @@ -63,7 +63,7 @@ class IconCollection: return constructed_icon - current_set: list[Union[str, dict[str, str]]] + current_set: List[Union[str, Dict[str, str]]] for matcher in scheme.node_matchers: matcher: NodeMatcher @@ -83,7 +83,7 @@ class IconCollection: continue for icon_id in matcher.under_icon: for icon_2_id in matcher.with_icon: - current_set: list[str] = ( + current_set: List[str] = ( [icon_id] + [icon_2_id] + matcher.over_icon ) add() @@ -102,7 +102,7 @@ class IconCollection: ): add() - specified_ids: set[str] = set() + specified_ids: Set[str] = set() for icon in icons: specified_ids |= set(icon.get_shape_ids()) diff --git a/map_machine/icon.py b/map_machine/icon.py index ea6792c..ad45078 100644 --- a/map_machine/icon.py +++ b/map_machine/icon.py @@ -6,7 +6,7 @@ import logging import re from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, List, Optional, Set from xml.etree import ElementTree from xml.etree.ElementTree import Element @@ -47,13 +47,13 @@ class Shape: id_: str # shape identifier name: Optional[str] = None # icon description is_right_directed: Optional[bool] = None - emojis: set[str] = field(default_factory=set) + emojis: Set[str] = field(default_factory=set) is_part: bool = False @classmethod def from_structure( cls, - structure: dict[str, Any], + structure: Dict[str, Any], path: str, offset: np.ndarray, id_: str, @@ -105,7 +105,7 @@ class Shape: :param offset: additional offset :param scale: scale resulting image """ - transformations: list[str] = [] + transformations: List[str] = [] shift: np.ndarray = point + offset transformations.append(f"translate({shift[0]},{shift[1]})") @@ -138,7 +138,7 @@ def verify_sketch_element(element: Element, id_: str) -> bool: if "style" not in element.attrib or not element.attrib["style"]: return True - style: dict[str, str] = dict( + style: Dict[str, str] = dict( [x.split(":") for x in element.attrib["style"].split(";")] ) if ( @@ -180,8 +180,8 @@ class ShapeExtractor: :param svg_file_name: input SVG file name with icons. File may contain any other irrelevant graphics. """ - self.shapes: dict[str, Shape] = {} - self.configuration: dict[str, Any] = json.load( + self.shapes: Dict[str, Shape] = {} + self.configuration: Dict[str, Any] = json.load( configuration_file_name.open() ) root: Element = ElementTree.parse(svg_file_name).getroot() @@ -229,7 +229,7 @@ class ShapeExtractor: name = child_node.text break - configuration: dict[str, Any] = ( + configuration: Dict[str, Any] = ( self.configuration[id_] if id_ in self.configuration else {} ) self.shapes[id_] = Shape.from_structure( @@ -271,7 +271,7 @@ class ShapeSpecification: self, svg: BaseElement, point: np.ndarray, - tags: dict[str, Any] = None, + tags: Dict[str, Any] = None, outline: bool = False, outline_opacity: float = 1.0, ) -> None: @@ -299,7 +299,7 @@ class ShapeSpecification: bright: bool = is_bright(self.color) color: Color = Color("black") if bright else Color("white") - style: dict[str, Any] = { + style: Dict[str, Any] = { "fill": color.hex, "stroke": color.hex, "stroke-width": 2.2, @@ -330,14 +330,14 @@ class Icon: Icon that consists of (probably) multiple shapes. """ - shape_specifications: list[ShapeSpecification] + shape_specifications: List[ShapeSpecification] opacity: float = 1.0 - def get_shape_ids(self) -> list[str]: + def get_shape_ids(self) -> List[str]: """Get all shape identifiers in the icon.""" return [x.shape.id_ for x in self.shape_specifications] - def get_names(self) -> list[str]: + def get_names(self) -> List[str]: """Get all shape names in the icon.""" return [ (x.shape.name if x.shape.name else "unknown") @@ -348,7 +348,7 @@ class Icon: self, svg: svgwrite.Drawing, point: np.ndarray, - tags: dict[str, Any] = None, + tags: Dict[str, Any] = None, outline: bool = False, ) -> None: """ @@ -420,7 +420,7 @@ class Icon: shape_specification.color = color def add_specifications( - self, specifications: list[ShapeSpecification] + self, specifications: List[ShapeSpecification] ) -> None: """Add shape specifications to the icon.""" self.shape_specifications += specifications @@ -443,8 +443,8 @@ class IconSet: """ main_icon: Icon - extra_icons: list[Icon] + extra_icons: List[Icon] # Tag keys that were processed to create icon set (other tag keys should be # displayed by text or ignored) - processed: set[str] + processed: Set[str] diff --git a/map_machine/mapcss.py b/map_machine/mapcss.py index 371748d..68530a2 100644 --- a/map_machine/mapcss.py +++ b/map_machine/mapcss.py @@ -4,7 +4,7 @@ MapCSS scheme creation. import argparse import logging from pathlib import Path -from typing import Optional, TextIO +from typing import Dict, List, Optional, TextIO from colour import Color @@ -84,8 +84,8 @@ class MapCSSWriter: self.add_icons_for_lifecycle: bool = add_icons_for_lifecycle self.icon_directory_name: str = icon_directory_name - self.point_matchers: list[Matcher] = scheme.node_matchers - self.line_matchers: list[Matcher] = scheme.way_matchers + self.point_matchers: List[Matcher] = scheme.node_matchers + self.line_matchers: List[Matcher] = scheme.way_matchers def add_selector( self, @@ -103,7 +103,7 @@ class MapCSSWriter: :param opacity: icon opacity :return: string representation of selector """ - elements: dict[str, str] = {} + elements: Dict[str, str] = {} clean_shapes = matcher.get_clean_shapes() if clean_shapes: @@ -116,7 +116,7 @@ class MapCSSWriter: if opacity is not None: elements["icon-opacity"] = f"{opacity:.2f}" - style: dict[str, str] = matcher.get_style() + style: Dict[str, str] = matcher.get_style() if style: if "fill" in style: elements["fill-color"] = style["fill"] diff --git a/map_machine/mapper.py b/map_machine/mapper.py index 6b978bc..27bef79 100644 --- a/map_machine/mapper.py +++ b/map_machine/mapper.py @@ -4,7 +4,7 @@ Simple OpenStreetMap renderer. import argparse import logging from pathlib import Path -from typing import Iterator, Optional +from typing import Dict, Iterator, List, Optional, Set import numpy as np import svgwrite @@ -57,7 +57,7 @@ class Map: self.svg.add( Rect((0, 0), self.flinger.size, fill=self.background_color) ) - ways: list[StyledFigure] = sorted( + ways: List[StyledFigure] = sorted( constructor.figures, key=lambda x: x.line_style.priority ) ways_length: int = len(ways) @@ -94,7 +94,7 @@ class Map: self.configuration.overlap, ) - nodes: list[Point] = sorted( + nodes: List[Point] = sorted( constructor.points, key=lambda x: -x.priority ) steps: int = len(nodes) @@ -156,7 +156,7 @@ class Map: def draw_roads(self, roads: Iterator[Road]) -> None: """Draw road as simple SVG path.""" - nodes: dict[OSMNode, set[RoadPart]] = {} + nodes: Dict[OSMNode, Set[RoadPart]] = {} for road in roads: for index in range(len(road.outers[0]) - 1): @@ -177,7 +177,7 @@ class Map: nodes[node_2].add(part_2) for node in nodes: - parts: set[RoadPart] = nodes[node] + parts: Set[RoadPart] = nodes[node] if len(parts) < 4: continue intersection: Intersection = Intersection(list(parts)) @@ -197,7 +197,7 @@ def ui(arguments: argparse.Namespace) -> None: cache_path.mkdir(parents=True, exist_ok=True) boundary_box: Optional[BoundaryBox] = None - input_file_names: list[Path] = [] + input_file_names: List[Path] = [] if arguments.input_file_names: input_file_names = list(map(Path, arguments.input_file_names)) diff --git a/map_machine/moire_manager.py b/map_machine/moire_manager.py index 23e9853..8d84b8b 100644 --- a/map_machine/moire_manager.py +++ b/map_machine/moire_manager.py @@ -4,7 +4,7 @@ Moire markup extension for Map Machine. import argparse from abc import ABC from pathlib import Path -from typing import Any, Union +from typing import Any, Dict, List, Union from moire.default import Default, DefaultHTML, DefaultMarkdown, DefaultWiki from moire.moire import Tag @@ -17,7 +17,7 @@ from map_machine.workspace import workspace __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -Arguments = list[Any] +Arguments = List[Any] Code = Union[str, Tag, list] PREFIX: str = "https://wiki.openstreetmap.org/wiki/" @@ -50,13 +50,13 @@ class ArgumentParser(argparse.ArgumentParser): """ def __init__(self, *args, **kwargs) -> None: - self.arguments: list[dict[str, Any]] = [] + self.arguments: List[Dict[str, Any]] = [] super(ArgumentParser, self).__init__(*args, **kwargs) def add_argument(self, *args, **kwargs) -> None: """Just store argument with options.""" super(ArgumentParser, self).add_argument(*args, **kwargs) - argument: dict[str, Any] = {"arguments": [x for x in args]} + argument: Dict[str, Any] = {"arguments": [x for x in args]} for key in kwargs: argument[key] = kwargs[key] diff --git a/map_machine/osm_getter.py b/map_machine/osm_getter.py index 13f34cb..7c946f5 100644 --- a/map_machine/osm_getter.py +++ b/map_machine/osm_getter.py @@ -5,6 +5,7 @@ import logging import time from dataclasses import dataclass from pathlib import Path +from typing import Dict import urllib3 @@ -48,7 +49,7 @@ def get_osm( return content.decode("utf-8") -def get_data(address: str, parameters: dict[str, str]) -> bytes: +def get_data(address: str, parameters: Dict[str, str]) -> bytes: """ Construct Internet page URL and get its descriptor. diff --git a/map_machine/osm_reader.py b/map_machine/osm_reader.py index 80c8f94..db6f843 100644 --- a/map_machine/osm_reader.py +++ b/map_machine/osm_reader.py @@ -7,7 +7,7 @@ import re from dataclasses import dataclass, field from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any, Dict, List, Optional, Set from xml.etree import ElementTree from xml.etree.ElementTree import Element @@ -27,7 +27,7 @@ MILES_PATTERN: re.Pattern = re.compile("^(?P\\d*\\.?\\d*)\\s*mi$") # See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay -STAGES_OF_DECAY: list[str] = [ +STAGES_OF_DECAY: List[str] = [ "disused", "abandoned", "ruins", @@ -47,7 +47,7 @@ def parse_float(string: str) -> Optional[float]: return None -def parse_levels(string: str) -> list[float]: +def parse_levels(string: str) -> List[float]: """Parse string representation of level sequence value.""" # TODO: add `-` parsing try: @@ -63,7 +63,7 @@ class Tagged: Something with tags (string to string mapping). """ - tags: dict[str, str] + tags: Dict[str, str] def get_tag(self, key: str) -> Optional[str]: """ @@ -127,7 +127,7 @@ class OSMNode(Tagged): def from_xml_structure(cls, element: Element) -> "OSMNode": """Parse node from OSM XML `` element.""" attributes = element.attrib - tags: dict[str, str] = dict( + tags: Dict[str, str] = dict( [(x.attrib["k"], x.attrib["v"]) for x in element if x.tag == "tag"] ) return cls( @@ -144,7 +144,7 @@ class OSMNode(Tagged): ) @classmethod - def parse_from_structure(cls, structure: dict[str, Any]) -> "OSMNode": + def parse_from_structure(cls, structure: Dict[str, Any]) -> "OSMNode": """ Parse node from Overpass-like structure. @@ -169,7 +169,7 @@ class OSMWay(Tagged): """ id_: int - nodes: Optional[list[OSMNode]] = field(default_factory=list) + nodes: Optional[List[OSMNode]] = field(default_factory=list) visible: Optional[str] = None changeset: Optional[str] = None timestamp: Optional[datetime] = None @@ -178,11 +178,11 @@ class OSMWay(Tagged): @classmethod def from_xml_structure( - cls, element: Element, nodes: dict[int, OSMNode] + cls, element: Element, nodes: Dict[int, OSMNode] ) -> "OSMWay": """Parse way from OSM XML `` element.""" attributes = element.attrib - tags: dict[str, str] = dict( + tags: Dict[str, str] = dict( [(x.attrib["k"], x.attrib["v"]) for x in element if x.tag == "tag"] ) return cls( @@ -200,7 +200,7 @@ class OSMWay(Tagged): @classmethod def parse_from_structure( - cls, structure: dict[str, Any], nodes: dict[int, OSMNode] + cls, structure: Dict[str, Any], nodes: Dict[int, OSMNode] ) -> "OSMWay": """ Parse way from Overpass-like structure. @@ -242,7 +242,7 @@ class OSMRelation(Tagged): """ id_: int - members: Optional[list[OSMMember]] + members: Optional[List[OSMMember]] visible: Optional[str] = None changeset: Optional[str] = None timestamp: Optional[datetime] = None @@ -253,8 +253,8 @@ class OSMRelation(Tagged): def from_xml_structure(cls, element: Element) -> "OSMRelation": """Parse relation from OSM XML `` element.""" attributes = element.attrib - members: list[OSMMember] = [] - tags: dict[str, str] = {} + members: List[OSMMember] = [] + tags: Dict[str, str] = {} for subelement in element: if subelement.tag == "member": subattributes = subelement.attrib @@ -281,7 +281,7 @@ class OSMRelation(Tagged): ) @classmethod - def parse_from_structure(cls, structure: dict[str, Any]) -> "OSMRelation": + def parse_from_structure(cls, structure: Dict[str, Any]) -> "OSMRelation": """ Parse relation from Overpass-like structure. @@ -311,12 +311,12 @@ class OSMData: """ def __init__(self) -> None: - self.nodes: dict[int, OSMNode] = {} - self.ways: dict[int, OSMWay] = {} - self.relations: dict[int, OSMRelation] = {} + self.nodes: Dict[int, OSMNode] = {} + self.ways: Dict[int, OSMWay] = {} + self.relations: Dict[int, OSMRelation] = {} - self.authors: set[str] = set() - self.levels: set[float] = set() + self.authors: Set[str] = set() + self.levels: Set[float] = set() self.time: MinMax = MinMax() self.view_box: Optional[BoundaryBox] = None self.equator_length: float = 40_075_017.0 @@ -364,8 +364,8 @@ class OSMData: with file_name.open() as input_file: structure = json.load(input_file) - node_map: dict[int, OSMNode] = {} - way_map: dict[int, OSMWay] = {} + node_map: Dict[int, OSMNode] = {} + way_map: Dict[int, OSMWay] = {} for element in structure["elements"]: if element["type"] == "node": diff --git a/map_machine/point.py b/map_machine/point.py index b72a36d..599fa06 100644 --- a/map_machine/point.py +++ b/map_machine/point.py @@ -1,7 +1,7 @@ """ Point: node representation on the map. """ -from typing import Optional +from typing import Dict, List, Optional, Set import numpy as np import svgwrite @@ -53,9 +53,9 @@ class Point(Tagged): def __init__( self, icon_set: IconSet, - labels: list[Label], - tags: dict[str, str], - processed: set[str], + labels: List[Label], + tags: Dict[str, str], + processed: Set[str], point: np.ndarray, priority: float = 0, is_for_node: bool = True, @@ -67,8 +67,8 @@ class Point(Tagged): assert point is not None self.icon_set: IconSet = icon_set - self.labels: list[Label] = labels - self.processed: set[str] = processed + self.labels: List[Label] = labels + self.processed: Set[str] = processed self.point: np.ndarray = point self.priority: float = priority self.layer: float = 0 @@ -92,7 +92,7 @@ class Point(Tagged): return position: np.ndarray = self.point + np.array((0, self.y)) - tags: Optional[dict[str, str]] = ( + tags: Optional[Dict[str, str]] = ( self.tags if self.add_tooltips else None ) self.main_icon_painted: bool = self.draw_point_shape( @@ -135,7 +135,7 @@ class Point(Tagged): icon: Icon, position: np.ndarray, occupied: Occupied, - tags: Optional[dict[str, str]] = None, + tags: Optional[Dict[str, str]] = None, ) -> bool: """Draw one combined icon and its outline.""" # Down-cast floats to integers to make icons pixel-perfect. @@ -166,7 +166,7 @@ class Point(Tagged): label_mode: str = LabelMode.MAIN, ) -> None: """Draw all labels.""" - labels: list[Label] + labels: List[Label] if label_mode == LabelMode.MAIN: labels = self.labels[:1] diff --git a/map_machine/road.py b/map_machine/road.py index 781968b..1ffba94 100644 --- a/map_machine/road.py +++ b/map_machine/road.py @@ -2,7 +2,7 @@ WIP: road shape drawing. """ from dataclasses import dataclass -from typing import Any, Optional +from typing import Any, Dict, List, Optional, Tuple import numpy as np import svgwrite @@ -62,7 +62,7 @@ class RoadPart: self, point_1: np.ndarray, point_2: np.ndarray, - lanes: list[Lane], + lanes: List[Lane], scale: float, ) -> None: """ @@ -72,7 +72,7 @@ class RoadPart: """ self.point_1: np.ndarray = point_1 self.point_2: np.ndarray = point_2 - self.lanes: list[Lane] = lanes + self.lanes: List[Lane] = lanes if lanes: self.width = sum(map(lambda x: x.get_width(scale), lanes)) else: @@ -284,8 +284,8 @@ class Intersection: points of the road parts should be the same. """ - def __init__(self, parts: list[RoadPart]) -> None: - self.parts: list[RoadPart] = sorted(parts, key=lambda x: x.get_angle()) + def __init__(self, parts: List[RoadPart]) -> None: + self.parts: List[RoadPart] = sorted(parts, key=lambda x: x.get_angle()) for index in range(len(self.parts)): next_index: int = 0 if index == len(self.parts) - 1 else index + 1 @@ -368,20 +368,20 @@ class Road(Tagged): def __init__( self, - tags: dict[str, str], - nodes: list[OSMNode], + tags: Dict[str, str], + nodes: List[OSMNode], matcher: RoadMatcher, flinger: Flinger, ) -> None: super().__init__(tags) - self.nodes: list[OSMNode] = nodes + self.nodes: List[OSMNode] = nodes self.matcher: RoadMatcher = matcher self.line: Polyline = Polyline( [flinger.fling(x.coordinates) for x in self.nodes] ) self.width: Optional[float] = matcher.default_width - self.lanes: list[Lane] = [] + self.lanes: List[Lane] = [] if "lanes" in tags: try: @@ -391,7 +391,7 @@ class Road(Tagged): pass if "width:lanes" in tags: - widths: list[float] = list( + widths: List[float] = list( map(float, tags["width:lanes"].split("|")) ) if len(widths) == len(self.lanes): @@ -434,7 +434,7 @@ class Road(Tagged): scale: float = flinger.get_scale(self.nodes[0].coordinates) path_commands: str = self.line.get_path() path: Path = Path(d=path_commands) - style: dict[str, Any] = { + style: Dict[str, Any] = { "fill": "none", "stroke": color.hex, "stroke-linecap": "butt", @@ -454,7 +454,7 @@ class Road(Tagged): -self.width / 2 + index * self.width / len(self.lanes) ) path: Path = Path(d=self.line.get_path(parallel_offset)) - style: dict[str, Any] = { + style: Dict[str, Any] = { "fill": "none", "stroke": color.hex, "stroke-linejoin": "round", @@ -467,7 +467,7 @@ class Road(Tagged): def get_curve_points( road: Road, scale: float, center: np.ndarray, road_end: np.ndarray -) -> list[np.ndarray]: +) -> List[np.ndarray]: """ :param road: road segment :param scale: current zoom scale @@ -492,11 +492,11 @@ class Connector: def __init__( self, - connections: list[tuple[Road, int]], + connections: List[Tuple[Road, int]], flinger: Flinger, scale: float, ) -> None: - self.connections: list[tuple[Road, int]] = connections + self.connections: List[Tuple[Road, int]] = connections self.road_1: Road = connections[0][0] self.index_1: int = connections[0][1] @@ -520,7 +520,7 @@ class SimpleConnector(Connector): def __init__( self, - connections: list[tuple[Road, int]], + connections: List[Tuple[Road, int]], flinger: Flinger, scale: float, ) -> None: @@ -558,7 +558,7 @@ class ComplexConnector(Connector): def __init__( self, - connections: list[tuple[Road, int]], + connections: List[Tuple[Road, int]], flinger: Flinger, scale: float, ) -> None: @@ -574,10 +574,10 @@ class ComplexConnector(Connector): node: OSMNode = self.road_1.nodes[self.index_1] point: np.ndarray = flinger.fling(node.coordinates) - points_1: list[np.ndarray] = get_curve_points( + points_1: List[np.ndarray] = get_curve_points( self.road_1, scale, point, self.road_1.line.points[self.index_1] ) - points_2: list[np.ndarray] = get_curve_points( + points_2: List[np.ndarray] = get_curve_points( self.road_2, scale, point, self.road_2.line.points[self.index_2] ) # fmt: off @@ -626,7 +626,7 @@ class SimpleIntersection(Connector): def __init__( self, - connections: list[tuple[Road, int]], + connections: List[Tuple[Road, int]], flinger: Flinger, scale: float, ) -> None: @@ -661,8 +661,8 @@ class Roads: """ def __init__(self) -> None: - self.roads: list[Road] = [] - self.connections: dict[int, list[tuple[Road, int]]] = {} + self.roads: List[Road] = [] + self.connections: Dict[int, List[Tuple[Road, int]]] = {} def append(self, road: Road) -> None: """Add road and update connections.""" @@ -679,8 +679,8 @@ class Roads: return scale: float = flinger.get_scale(self.roads[0].nodes[0].coordinates) - layered_roads: dict[float, list[Road]] = {} - layered_connectors: dict[float, list[Connector]] = {} + layered_roads: Dict[float, List[Road]] = {} + layered_connectors: Dict[float, List[Connector]] = {} for road in self.roads: if road.layer not in layered_roads: @@ -688,7 +688,7 @@ class Roads: layered_roads[road.layer].append(road) for id_ in self.connections: - connected: list[tuple[Road, int]] = self.connections[id_] + connected: List[Tuple[Road, int]] = self.connections[id_] connector: Connector if len(self.connections[id_]) == 2: @@ -706,10 +706,10 @@ class Roads: layered_connectors[connector.layer].append(connector) for layer in sorted(layered_roads.keys()): - roads: list[Road] = sorted( + roads: List[Road] = sorted( layered_roads[layer], key=lambda x: x.matcher.priority ) - connectors: list[Connector] + connectors: List[Connector] if layer in layered_connectors: connectors = layered_connectors[layer] else: diff --git a/map_machine/scheme.py b/map_machine/scheme.py index 2f3e4b5..70e4073 100644 --- a/map_machine/scheme.py +++ b/map_machine/scheme.py @@ -5,7 +5,7 @@ import logging from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Any, Optional, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union import numpy as np import yaml @@ -27,7 +27,7 @@ from map_machine.text import Label, get_address, get_text __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -IconDescription = list[Union[str, dict[str, str]]] +IconDescription = List[Union[str, Dict[str, str]]] @dataclass @@ -36,7 +36,7 @@ class LineStyle: SVG line style and its priority. """ - style: dict[str, Union[int, float, str]] + style: Dict[str, Union[int, float, str]] priority: float = 0.0 @@ -54,7 +54,7 @@ class MatchingType(Enum): def is_matched_tag( matcher_tag_key: str, matcher_tag_value: Union[str, list], - tags: dict[str, str], + tags: Dict[str, str], ) -> MatchingType: """ Check whether element tags contradict tag matcher. @@ -90,7 +90,7 @@ def get_selector(key: str, value: str, prefix: str = "") -> str: return f'[{key}="{value}"]' -def match_location(restrictions: dict[str, str], country: str) -> bool: +def match_location(restrictions: Dict[str, str], country: str) -> bool: """Check whether country is matched by location restrictions.""" if "exclude" in restrictions and country in restrictions["exclude"]: return False @@ -109,11 +109,11 @@ class Matcher: """ def __init__( - self, structure: dict[str, Any], group: Optional[dict[str, Any]] = None + self, structure: Dict[str, Any], group: Optional[Dict[str, Any]] = None ) -> None: - self.tags: dict[str, str] = structure["tags"] + self.tags: Dict[str, str] = structure["tags"] - self.exception: dict[str, str] = {} + self.exception: Dict[str, str] = {} if "exception" in structure: self.exception = structure["exception"] @@ -125,7 +125,7 @@ class Matcher: if "replace_shapes" in structure: self.replace_shapes = structure["replace_shapes"] - self.location_restrictions: dict[str, str] = {} + self.location_restrictions: Dict[str, str] = {} if "location_restrictions" in structure: self.location_restrictions = structure["location_restrictions"] @@ -137,7 +137,7 @@ class Matcher: def is_matched( self, - tags: dict[str, str], + tags: Dict[str, str], configuration: Optional[MapConfiguration] = None, ) -> bool: """ @@ -186,11 +186,11 @@ class Matcher: [get_selector(x, y, prefix) for (x, y) in self.tags.items()] ) - def get_clean_shapes(self) -> Optional[list[str]]: + def get_clean_shapes(self) -> Optional[List[str]]: """Get list of shape identifiers for shapes.""" return None - def get_style(self) -> dict[str, Any]: + def get_style(self) -> Dict[str, Any]: """Return way SVG style.""" return {} @@ -201,7 +201,7 @@ class NodeMatcher(Matcher): """ def __init__( - self, structure: dict[str, Any], group: dict[str, Any] + self, structure: Dict[str, Any], group: Dict[str, Any] ) -> None: # Dictionary with tag keys and values, value lists, or "*" super().__init__(structure, group) @@ -238,7 +238,7 @@ class NodeMatcher(Matcher): if "with_icon" in structure: self.with_icon = structure["with_icon"] - def get_clean_shapes(self) -> Optional[list[str]]: + def get_clean_shapes(self) -> Optional[List[str]]: """Get list of shape identifiers for shapes.""" if not self.shapes: return None @@ -250,11 +250,11 @@ class WayMatcher(Matcher): Special tag matcher for ways. """ - def __init__(self, structure: dict[str, Any], scheme: "Scheme") -> None: + def __init__(self, structure: Dict[str, Any], scheme: "Scheme") -> None: super().__init__(structure) - self.style: dict[str, Any] = {"fill": "none"} + self.style: Dict[str, Any] = {"fill": "none"} if "style" in structure: - style: dict[str, Any] = structure["style"] + style: Dict[str, Any] = structure["style"] for key in style: if str(style[key]).endswith("_color"): self.style[key] = scheme.get_color(style[key]).hex.upper() @@ -264,7 +264,7 @@ class WayMatcher(Matcher): if "priority" in structure: self.priority = structure["priority"] - def get_style(self) -> dict[str, Any]: + def get_style(self) -> Dict[str, Any]: """Return way SVG style.""" return self.style @@ -274,7 +274,7 @@ class RoadMatcher(Matcher): Special tag matcher for highways. """ - def __init__(self, structure: dict[str, Any], scheme: "Scheme") -> None: + def __init__(self, structure: Dict[str, Any], scheme: "Scheme") -> None: super().__init__(structure) self.border_color: Color = Color( scheme.get_color(structure["border_color"]) @@ -287,7 +287,7 @@ class RoadMatcher(Matcher): if "priority" in structure: self.priority = structure["priority"] - def get_priority(self, tags: dict[str, str]) -> float: + def get_priority(self, tags: Dict[str, str]) -> float: layer: float = 0 if "layer" in tags: layer = float(tags.get("layer")) @@ -307,33 +307,33 @@ class Scheme: specification """ with file_name.open() as input_file: - content: dict[str, Any] = yaml.load( + content: Dict[str, Any] = yaml.load( input_file.read(), Loader=yaml.FullLoader ) - self.node_matchers: list[NodeMatcher] = [] + self.node_matchers: List[NodeMatcher] = [] for group in content["node_icons"]: for element in group["tags"]: self.node_matchers.append(NodeMatcher(element, group)) - self.colors: dict[str, str] = content["colors"] - self.material_colors: dict[str, str] = content["material_colors"] + self.colors: Dict[str, str] = content["colors"] + self.material_colors: Dict[str, str] = content["material_colors"] - self.way_matchers: list[WayMatcher] = [ + self.way_matchers: List[WayMatcher] = [ WayMatcher(x, self) for x in content["ways"] ] - self.road_matchers: list[RoadMatcher] = [ + self.road_matchers: List[RoadMatcher] = [ RoadMatcher(x, self) for x in content["roads"] ] - self.area_matchers: list[Matcher] = [ + self.area_matchers: List[Matcher] = [ Matcher(x) for x in content["area_tags"] ] - self.tags_to_write: list[str] = content["tags_to_write"] - self.prefix_to_write: list[str] = content["prefix_to_write"] - self.tags_to_skip: list[str] = content["tags_to_skip"] - self.prefix_to_skip: list[str] = content["prefix_to_skip"] + self.tags_to_write: List[str] = content["tags_to_write"] + self.prefix_to_write: List[str] = content["prefix_to_write"] + self.tags_to_skip: List[str] = content["tags_to_skip"] + self.prefix_to_skip: List[str] = content["prefix_to_skip"] # Storage for created icon sets. - self.cache: dict[str, tuple[IconSet, int]] = {} + self.cache: Dict[str, Tuple[IconSet, int]] = {} def get_color(self, color: str) -> Color: """ @@ -384,10 +384,10 @@ class Scheme: def get_icon( self, extractor: ShapeExtractor, - tags: dict[str, Any], - processed: set[str], + tags: Dict[str, Any], + processed: Set[str], configuration: MapConfiguration = MapConfiguration(), - ) -> tuple[Optional[IconSet], int]: + ) -> Tuple[Optional[IconSet], int]: """ Construct icon set. @@ -404,7 +404,7 @@ class Scheme: return self.cache[tags_hash] main_icon: Optional[Icon] = None - extra_icons: list[Icon] = [] + extra_icons: List[Icon] = [] priority: int = 0 index: int = 0 @@ -419,7 +419,7 @@ class Scheme: and not matcher.check_zoom_level(configuration.zoom_level) ): return None, 0 - matcher_tags: set[str] = set(matcher.tags.keys()) + matcher_tags: Set[str] = set(matcher.tags.keys()) priority = len(self.node_matchers) - index if not matcher.draw: processed |= matcher_tags @@ -492,7 +492,7 @@ class Scheme: return returned, priority - def get_style(self, tags: dict[str, Any]) -> list[LineStyle]: + def get_style(self, tags: Dict[str, Any]) -> List[LineStyle]: """Get line style based on tags and scale.""" line_styles = [] @@ -504,7 +504,7 @@ class Scheme: return line_styles - def get_road(self, tags: dict[str, Any]) -> Optional[RoadMatcher]: + def get_road(self, tags: Dict[str, Any]) -> Optional[RoadMatcher]: """Get road matcher if tags are matched.""" for matcher in self.road_matchers: if not matcher.is_matched(tags): @@ -513,10 +513,10 @@ class Scheme: return None def construct_text( - self, tags: dict[str, str], draw_captions: str, processed: set[str] - ) -> list[Label]: + self, tags: Dict[str, str], draw_captions: str, processed: Set[str] + ) -> List[Label]: """Construct labels for not processed tags.""" - texts: list[Label] = [] + texts: List[Label] = [] name = None alt_name = None @@ -542,7 +542,7 @@ class Scheme: alt_name = "" alt_name += "ex " + tags["old_name"] - address: list[str] = get_address(tags, draw_captions, processed) + address: List[str] = get_address(tags, draw_captions, processed) if name: texts.append(Label(name, Color("black"))) @@ -587,7 +587,7 @@ class Scheme: texts.append(Label(tags[tag])) return texts - def is_area(self, tags: dict[str, str]) -> bool: + def is_area(self, tags: Dict[str, str]) -> bool: """Check whether way described by tags is area.""" for matcher in self.area_matchers: if matcher.is_matched(tags): @@ -595,7 +595,7 @@ class Scheme: return False def process_ignored( - self, tags: dict[str, str], processed: set[str] + self, tags: Dict[str, str], processed: Set[str] ) -> None: """ Mark all ignored tag as processed. @@ -607,7 +607,7 @@ class Scheme: def get_shape_specification( self, - structure: Union[str, dict[str, Any]], + structure: Union[str, Dict[str, Any]], extractor: ShapeExtractor, color: Color = DEFAULT_COLOR, ) -> ShapeSpecification: diff --git a/map_machine/server.py b/map_machine/server.py index ad0d3f3..914797e 100644 --- a/map_machine/server.py +++ b/map_machine/server.py @@ -5,7 +5,7 @@ import argparse import logging from http.server import HTTPServer, SimpleHTTPRequestHandler from pathlib import Path -from typing import Optional +from typing import List, Optional, Tuple import cairosvg @@ -28,14 +28,14 @@ class _Handler(SimpleHTTPRequestHandler): def __init__( self, request: bytes, - client_address: tuple[str, int], + client_address: Tuple[str, int], server: HTTPServer, ) -> None: super().__init__(request, client_address, server) def do_GET(self) -> None: """Serve a GET request.""" - parts: list[str] = self.path.split("/") + parts: List[str] = self.path.split("/") if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"): return diff --git a/map_machine/taginfo.py b/map_machine/taginfo.py index 94c0cd2..a819154 100644 --- a/map_machine/taginfo.py +++ b/map_machine/taginfo.py @@ -7,6 +7,7 @@ import json import logging from datetime import datetime from pathlib import Path +from typing import List from map_machine import ( __author__, @@ -54,7 +55,7 @@ class TaginfoProjectFile: ): key: str = list(matcher.tags.keys())[0] value: str = matcher.tags[key] - ids: list[str] = [ + ids: List[str] = [ (x if isinstance(x, str) else x["shape"]) for x in matcher.shapes ] diff --git a/map_machine/text.py b/map_machine/text.py index 4ac6058..19d02a3 100644 --- a/map_machine/text.py +++ b/map_machine/text.py @@ -2,7 +2,7 @@ OSM address tag processing. """ from dataclasses import dataclass -from typing import Any +from typing import Any, Dict, List, Set from colour import Color @@ -25,8 +25,8 @@ class Label: def get_address( - tags: dict[str, Any], draw_captions_mode: str, processed: set[str] -) -> list[str]: + tags: Dict[str, Any], draw_captions_mode: str, processed: Set[str] +) -> List[str]: """ Construct address text list from the tags. @@ -34,7 +34,7 @@ def get_address( :param draw_captions_mode: captions mode ("all", "main", or "no") :param processed: set of processed tag keys """ - address: list[str] = [] + address: List[str] = [] if draw_captions_mode == "address": if "addr:postcode" in tags: @@ -80,10 +80,10 @@ def format_frequency(value: str) -> str: return f"{value} " -def get_text(tags: dict[str, Any], processed: set[str]) -> list[Label]: +def get_text(tags: Dict[str, Any], processed: Set[str]) -> List[Label]: """Get text representation of writable tags.""" - texts: list[Label] = [] - values: list[str] = [] + texts: List[Label] = [] + values: List[str] = [] if "voltage:primary" in tags: values.append(tags["voltage:primary"]) diff --git a/map_machine/tile.py b/map_machine/tile.py index 4ac46a8..61de2bd 100644 --- a/map_machine/tile.py +++ b/map_machine/tile.py @@ -8,7 +8,7 @@ import logging import sys from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import List, Optional, Tuple import cairosvg import numpy as np @@ -72,7 +72,7 @@ class Tile: lat_deg: np.ndarray = np.degrees(lat_rad) return np.array((lat_deg, lon_deg)) - def get_boundary_box(self) -> tuple[np.ndarray, np.ndarray]: + def get_boundary_box(self) -> Tuple[np.ndarray, np.ndarray]: """ Get geographical boundary box of the tile: north-west and south-east points. @@ -191,11 +191,11 @@ class Tile: cairosvg.svg2png(file_obj=input_file, write_to=str(output_path)) logging.info(f"SVG file is rasterized to {output_path}.") - def subdivide(self, zoom_level: int) -> list["Tile"]: + def subdivide(self, zoom_level: int) -> List["Tile"]: """Get subtiles of the tile.""" assert zoom_level >= self.zoom_level - tiles: list["Tile"] = [] + tiles: List["Tile"] = [] n: int = 2 ** (zoom_level - self.zoom_level) for i in range(n): for j in range(n): @@ -214,7 +214,7 @@ class Tiles: Collection of tiles. """ - tiles: list[Tile] + tiles: List[Tile] tile_1: Tile # Left top tile. tile_2: Tile # Right bottom tile. zoom_level: int # OpenStreetMap zoom level. @@ -230,7 +230,7 @@ class Tiles: :param boundary_box: area to be covered by tiles :param zoom_level: zoom level in OpenStreetMap terminology """ - tiles: list[Tile] = [] + tiles: List[Tile] = [] tile_1: Tile = Tile.from_coordinates( boundary_box.get_left_top(), zoom_level ) @@ -327,7 +327,7 @@ class Tiles: for tile in self.tiles: x: int = tile.x - self.tile_1.x y: int = tile.y - self.tile_1.y - area: tuple[int, int, int, int] = ( + area: Tuple[int, int, int, int] = ( x * TILE_WIDTH, y * TILE_HEIGHT, (x + 1) * TILE_WIDTH, @@ -413,7 +413,7 @@ class Tiles: def subdivide(self, zoom_level: int) -> "Tiles": """Get subtiles from tiles.""" - tiles: list[Tile] = [] + tiles: List[Tile] = [] for tile in self.tiles: tiles += tile.subdivide(zoom_level) return Tiles( @@ -429,9 +429,9 @@ class ScaleConfigurationException(Exception): """Wrong configuration format.""" -def parse_zoom_level(zoom_level_specification: str) -> list[int]: +def parse_zoom_level(zoom_level_specification: str) -> List[int]: """Parse zoom level specification.""" - parts: list[str] + parts: List[str] if "," in zoom_level_specification: parts = zoom_level_specification.split(",") else: @@ -444,7 +444,7 @@ def parse_zoom_level(zoom_level_specification: str) -> list[int]: raise ScaleConfigurationException("Scale is too big.") return parsed_zoom_level - result: list[int] = [] + result: List[int] = [] for part in parts: if "-" in part: from_, to = part.split("-") @@ -463,7 +463,7 @@ def ui(options: argparse.Namespace) -> None: """Simple user interface for tile generation.""" directory: Path = workspace.get_tile_path() - zoom_levels: list[int] = parse_zoom_level(options.zoom) + zoom_levels: List[int] = parse_zoom_level(options.zoom) min_zoom_level: int = min(zoom_levels) if options.input_file_name: @@ -478,7 +478,7 @@ def ui(options: argparse.Namespace) -> None: ) tiles.draw(directory, Path(options.cache), configuration, osm_data) elif options.coordinates: - coordinates: list[float] = list( + coordinates: List[float] = list( map(float, options.coordinates.strip().split(",")) ) min_tile: Tile = Tile.from_coordinates( diff --git a/map_machine/ui.py b/map_machine/ui.py index e1d2283..07a691c 100644 --- a/map_machine/ui.py +++ b/map_machine/ui.py @@ -3,6 +3,7 @@ Command-line user interface. """ import argparse import sys +from typing import Dict, List from map_machine import __version__ from map_machine.map_configuration import BuildingMode, DrawingMode, LabelMode @@ -14,7 +15,7 @@ __email__ = "me@enzet.ru" BOXES: str = " ▏▎▍▌▋▊▉" BOXES_LENGTH: int = len(BOXES) -COMMANDS: dict[str, list[str]] = { +COMMANDS: Dict[str, List[str]] = { "render": ["render", "-b", "10.000,20.000,10.001,20.001"], "render_with_tooltips": [ "render", @@ -29,7 +30,7 @@ COMMANDS: dict[str, list[str]] = { } -def parse_arguments(args: list[str]) -> argparse.Namespace: +def parse_arguments(args: List[str]) -> argparse.Namespace: """Parse Map Machine command-line arguments.""" parser: argparse.ArgumentParser = argparse.ArgumentParser( description="Map Machine. OpenStreetMap renderer with custom icon set" @@ -117,9 +118,15 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--show-tooltips", help="add tooltips with tags for icons in SVG files", - action=argparse.BooleanOptionalAction, + action="store_true", default=False, ) + parser.add_argument( + "--no-show-tooltips", + dest="show_tooltips", + help="don't add tooltips with tags for icons in SVG files", + action="store_false", + ) parser.add_argument( "--country", help="two-letter code (ISO 3166-1 alpha-2) of country, that should be " @@ -256,24 +263,42 @@ def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None: """Add arguments for mapcss command.""" parser.add_argument( "--icons", - action=argparse.BooleanOptionalAction, + action="store_true", default=True, help="add icons for nodes and areas", ) + parser.add_argument( + "--no-icons", + dest="icons", + action="store_false", + help="don't add icons for nodes and areas", + ) parser.add_argument( "--ways", - action=argparse.BooleanOptionalAction, + action="store_true", default=False, help="add style for ways and relations", ) + parser.add_argument( + "--no-ways", + dest="ways", + action="store_false", + help="don't add style for ways and relations", + ) parser.add_argument( "--lifecycle", - action=argparse.BooleanOptionalAction, + action="store_true", default=True, help="add icons for lifecycle tags; be careful: this will increase the " f"number of node and area selectors by {len(STAGES_OF_DECAY) + 1} " f"times", ) + parser.add_argument( + "--no-lifecycle", + dest="lifecycle", + action="store_false", + help="don't add icons for lifecycle tags", + ) def progress_bar( diff --git a/map_machine/vector.py b/map_machine/vector.py index c852c4f..f2794fd 100644 --- a/map_machine/vector.py +++ b/map_machine/vector.py @@ -1,6 +1,8 @@ """ Vector utility. """ +from typing import List + import numpy as np __author__ = "Sergey Vartanov" @@ -45,12 +47,12 @@ class Polyline: List of connected points. """ - def __init__(self, points: list[np.ndarray]) -> None: - self.points: list[np.ndarray] = points + def __init__(self, points: List[np.ndarray]) -> None: + self.points: List[np.ndarray] = points def get_path(self, parallel_offset: float = 0) -> str: """Construct SVG path commands.""" - points: list[np.ndarray] + points: List[np.ndarray] try: points = ( LineString(self.points).parallel_offset(parallel_offset).coords diff --git a/setup.py b/setup.py index 699f3e0..12693e3 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ setup( "scheme/default.yml", ], }, - python_requires=">=3.9", + python_requires=">=3.8", install_requires=[ "CairoSVG>=2.5.0", "colour>=0.1.5", diff --git a/tests/test_command_line.py b/tests/test_command_line.py index 6da6104..5fa2e31 100644 --- a/tests/test_command_line.py +++ b/tests/test_command_line.py @@ -7,13 +7,15 @@ from subprocess import PIPE, Popen __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" +from typing import List + from xml.etree import ElementTree from xml.etree.ElementTree import Element from map_machine.ui import COMMANDS -def error_run(arguments: list[str], message: bytes) -> None: +def error_run(arguments: List[str], message: bytes) -> None: """Run command that should fail and check error message.""" p = Popen(["map-machine"] + arguments, stderr=PIPE) _, error = p.communicate() @@ -21,7 +23,7 @@ def error_run(arguments: list[str], message: bytes) -> None: assert error == message -def run(arguments: list[str], message: bytes) -> None: +def run(arguments: List[str], message: bytes) -> None: """Run command that should fail and check error message.""" p = Popen(["map-machine"] + arguments, stderr=PIPE) _, error = p.communicate() diff --git a/tests/test_icons.py b/tests/test_icons.py index 49d719d..cd18024 100644 --- a/tests/test_icons.py +++ b/tests/test_icons.py @@ -1,6 +1,8 @@ """ Test icon generation for nodes. """ +from typing import Dict, Set + import pytest from map_machine.grid import IconCollection @@ -32,9 +34,9 @@ def test_icons_by_name(init_collection: IconCollection) -> None: init_collection.draw_icons(workspace.get_icons_by_name_path(), by_name=True) -def get_icon(tags: dict[str, str]) -> IconSet: +def get_icon(tags: Dict[str, str]) -> IconSet: """Construct icon from tags.""" - processed: set[str] = set() + processed: Set[str] = set() icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed) return icon diff --git a/tests/test_label.py b/tests/test_label.py index 23b7a60..7b54345 100644 --- a/tests/test_label.py +++ b/tests/test_label.py @@ -1,6 +1,8 @@ """ Test label generation for nodes. """ +from typing import Dict, List, Set + from map_machine.text import Label from tests import SCHEME @@ -8,9 +10,9 @@ __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -def construct_labels(tags: dict[str, str]) -> list[Label]: +def construct_labels(tags: Dict[str, str]) -> List[Label]: """Construct labels from OSM node tags.""" - processed: set[str] = set() + processed: Set[str] = set() return SCHEME.construct_text(tags, "all", processed)