diff --git a/data/dictionary.xml b/data/dictionary.xml index e2e904f..f5b4b0b 100644 --- a/data/dictionary.xml +++ b/data/dictionary.xml @@ -36,6 +36,7 @@ rasterized röntgen scoria + skillion subattributes subelement subparser diff --git a/data/githooks/pre-commit b/data/githooks/pre-commit index 012262d..571fb45 100755 --- a/data/githooks/pre-commit +++ b/data/githooks/pre-commit @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash python_files="map_machine setup.py tests data/githooks/commit-msg" diff --git a/data/githooks/pre-push b/data/githooks/pre-push index 2576f9a..c748aef 100755 --- a/data/githooks/pre-push +++ b/data/githooks/pre-push @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash echo "Looking for changes..." files=`git status --porcelain | wc -l` diff --git a/map_machine/__init__.py b/map_machine/__init__.py index 2ea2764..fb38765 100644 --- a/map_machine/__init__.py +++ b/map_machine/__init__.py @@ -11,7 +11,7 @@ __url__ = "https://github.com/enzet/map-machine" __doc_url__ = f"{__url__}/blob/main/README.md" __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -__version__ = "0.1.4" +__version__ = "0.1.5" REQUIREMENTS = [ "CairoSVG>=2.5.0", diff --git a/map_machine/constructor.py b/map_machine/constructor.py index 49abc4c..af15fa7 100644 --- a/map_machine/constructor.py +++ b/map_machine/constructor.py @@ -15,10 +15,8 @@ from map_machine.feature.building import Building, BUILDING_SCALE from map_machine.feature.crater import Crater from map_machine.feature.direction import DirectionSector from map_machine.feature.road import Road, Roads -from map_machine.figure import ( - StyledFigure, -) from map_machine.feature.tree import Tree +from map_machine.figure import StyledFigure from map_machine.geometry.flinger import Flinger from map_machine.map_configuration import DrawingMode, MapConfiguration from map_machine.osm.osm_reader import ( diff --git a/map_machine/doc/doc_collections.py b/map_machine/doc/doc_collections.py index a892746..fcddc5a 100644 --- a/map_machine/doc/doc_collections.py +++ b/map_machine/doc/doc_collections.py @@ -9,8 +9,8 @@ from typing import Any, Optional, List, Dict, Set import numpy as np import svgwrite from svgwrite import Drawing -from svgwrite.text import Text from svgwrite.shapes import Line, Rect +from svgwrite.text import Text from map_machine.map_configuration import MapConfiguration from map_machine.osm.osm_reader import Tags diff --git a/map_machine/element/test_elements.py b/map_machine/doc/draw_elements.py similarity index 53% rename from map_machine/element/test_elements.py rename to map_machine/doc/draw_elements.py index d80a7ba..df016b2 100644 --- a/map_machine/element/test_elements.py +++ b/map_machine/doc/draw_elements.py @@ -7,14 +7,21 @@ from typing import Dict, List, Optional, Tuple import numpy as np from svgwrite import Drawing +from svgwrite.text import Text -from map_machine.geometry.boundary_box import BoundaryBox from map_machine.constructor import Constructor +from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.flinger import Flinger -from map_machine.pictogram.icon import ShapeExtractor from map_machine.map_configuration import MapConfiguration from map_machine.mapper import Map -from map_machine.osm.osm_reader import OSMData, OSMNode, OSMWay +from map_machine.osm.osm_reader import OSMData, OSMNode, OSMWay, Tags +from map_machine.osm.tags import ( + HIGHWAY_VALUES, + AEROWAY_VALUES, + RAILWAY_VALUES, + ROAD_VALUES, +) +from map_machine.pictogram.icon import ShapeExtractor from map_machine.scheme import Scheme from map_machine.workspace import Workspace @@ -26,45 +33,6 @@ SHAPE_EXTRACTOR: ShapeExtractor = ShapeExtractor( ) DEFAULT_ZOOM: float = 18.0 - -HIGHWAY_VALUES: List[str] = [ - "motorway", - "trunk", - "primary", - "secondary", - "tertiary", - "unclassified", - "residential", - "service", - "service_minor", - "road", - "pedestrian", - "living_street", - "bridleway", - "cycleway", - "footway", - "steps", - "path", - "track", - "raceway", -] - -AEROWAY_VALUES: List[str] = [ - "runway", - "taxiway", -] - -RAILWAY_TAGS: List[Dict[str, str]] = [ - {"railway": "rail"}, - {"railway": "light_rail"}, - {"railway": "monorail"}, - {"railway": "funicular"}, - {"railway": "narrow_gauge"}, - {"railway": "subway"}, - {"railway": "subway", "color": "red"}, - {"railway": "subway", "color": "blue"}, -] - ROAD_WIDTHS_AND_FEATURES: List[Dict[str, str]] = [ {"width": "4"}, {"width": "8"}, @@ -79,8 +47,6 @@ ROAD_WIDTHS_AND_FEATURES: List[Dict[str, str]] = [ {"embankment": "yes", "width": "4"}, {"embankment": "yes", "width": "8"}, ] - - ROAD_LANES_AND_FEATURES: List[Dict[str, str]] = [ {"lanes": "1"}, {"lanes": "2"}, @@ -106,7 +72,6 @@ PLACEMENT_FEATURES_1: List[Dict[str, str]] = [ {"placement": "transition"}, {"lanes": "3", "placement": "right_of:1"}, # or placement=left_of:2 ] - PLACEMENT_FEATURES_2: List[Dict[str, str]] = [ {"lanes": "2"}, # or placement:backward=left_of:1 @@ -120,16 +85,18 @@ PLACEMENT_FEATURES_2: List[Dict[str, str]] = [ class Grid: """Creating map with elements ordered in grid.""" - def __init__(self) -> None: - self.x_step: float = 0.0002 - self.y_step: float = 0.0003 - self.x_start: float = 0.0 + def __init__(self, x_step: float = 0.0002, y_step: float = 0.0003): + self.x_step: float = x_step + self.y_step: float = y_step self.index: int = 0 self.nodes: Dict[OSMNode, Tuple[int, int]] = {} self.max_j: float = 0 self.max_i: float = 0 + self.way_id: int = 0 + self.osm_data: OSMData = OSMData() + self.texts: list[tuple[str, int, int]] = [] - def add_node(self, tags: Dict[str, str], i: int, j: int) -> OSMNode: + def add_node(self, tags: Tags, i: int, j: int) -> OSMNode: """Add OSM node to the grid.""" self.index += 1 node: OSMNode = OSMNode( @@ -142,21 +109,79 @@ class Grid: self.max_i = max(self.max_i, i * self.y_step) return node + def add_way(self, tags: Tags, nodes: list[OSMNode]) -> None: + """Add OSM way to the grid.""" + osm_way: OSMWay = OSMWay(tags, self.way_id, nodes) + self.osm_data.add_way(osm_way) + self.way_id += 1 + + def add_text(self, text: str, i: int, j: int) -> None: + self.texts.append((text, i, j)) + def get_boundary_box(self) -> BoundaryBox: """Compute resulting boundary box with margin of one grid step.""" return BoundaryBox( - -self.x_step, - -self.max_i - self.y_step, - self.max_j + self.x_step, - self.y_step, + -self.x_step * 1.5, + -self.max_i - self.y_step * 1.5, + self.max_j + self.x_step * 1.5, + self.y_step * 1.5, ) + def draw(self, output_path: Path, zoom: float = DEFAULT_ZOOM) -> None: + """Draw grid.""" + configuration: MapConfiguration = MapConfiguration( + level="all", credit=None + ) + flinger: Flinger = Flinger( + self.get_boundary_box(), zoom, self.osm_data.equator_length + ) + svg: Drawing = Drawing(output_path.name, flinger.size) + constructor: Constructor = Constructor( + self.osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, configuration + ) + constructor.construct() + map_: Map = Map(flinger, svg, SCHEME, configuration) + map_.draw(constructor) -def road_features( + for text, i, j in self.texts: + svg.add( + Text( + text, + flinger.fling((-i * self.y_step, j * self.x_step)) + (0, 3), + font_family="JetBrains Mono", + font_size=12, + ) + ) + + with output_path.open("w") as output_file: + svg.write(output_file) + logging.info(f"Map is drawn to {output_path}.") + + +def draw_overlapped_ways(types: list[dict[str, str]], path: Path) -> None: + """ + Draw two sets of ways intersecting each other to show how they overlapping. + """ + grid: Grid = Grid(0.00012, 0.00012) + + for index, tags in enumerate(types): + node_1: OSMNode = grid.add_node({}, index + 1, 8) + node_2: OSMNode = grid.add_node({}, index + 1, len(types) + 9) + grid.add_way(tags, [node_1, node_2]) + grid.add_text(", ".join(f"{k}={tags[k]}" for k in tags), index + 1, 0) + + for index, tags in enumerate(types): + node_1: OSMNode = grid.add_node({}, 0, index + 9) + node_2: OSMNode = grid.add_node({}, len(types) + 1, index + 9) + grid.add_way(tags, [node_1, node_2]) + + grid.draw(path) + + +def draw_road_features( types: List[Dict[str, str]], features: List[Dict[str, str]], path: Path ) -> None: """Draw test image with different road features.""" - osm_data: OSMData = OSMData() grid: Grid = Grid() for i, type_ in enumerate(types): @@ -168,58 +193,41 @@ def road_features( if previous: tags: Dict[str, str] = dict(type_) tags |= dict(features[j - 1]) - way: OSMWay = OSMWay( - tags, i * (len(features) + 1) + j, [previous, node] - ) - osm_data.add_way(way) + grid.add_way(tags, [previous, node]) previous = node - draw(osm_data, path, grid.get_boundary_box()) - - -def draw( - osm_data: OSMData, - output_path: Path, - boundary_box: BoundaryBox, - zoom: float = DEFAULT_ZOOM, -) -> None: - """Draw map.""" - configuration: MapConfiguration = MapConfiguration(level="all") - - flinger: Flinger = Flinger(boundary_box, zoom, osm_data.equator_length) - svg: Drawing = Drawing(output_path.name, flinger.size) - constructor: Constructor = Constructor( - osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, configuration - ) - constructor.construct() - map_: Map = Map(flinger, svg, SCHEME, configuration) - map_.draw(constructor) - - with output_path.open("w") as output_file: - svg.write(output_file) - logging.info(f"Map is drawn to {output_path}.") + grid.draw(path) if __name__ == "__main__": logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO) + out_path: Path = Path("out") + + road_tags: List[Dict[str, str]] = [ + {"highway": value} for value in ROAD_VALUES + ] highway_tags: List[Dict[str, str]] = [ {"highway": value} for value in HIGHWAY_VALUES ] aeroway_tags: List[Dict[str, str]] = [ {"aeroway": value} for value in AEROWAY_VALUES ] + railway_tags: list[dict[str, str]] = [ + {"railway": value} for value in RAILWAY_VALUES + ] - road_features( - highway_tags, ROAD_LANES_AND_FEATURES, Path("out") / "lanes.svg" + draw_road_features( + highway_tags, ROAD_LANES_AND_FEATURES, out_path / "lanes.svg" ) - road_features( - highway_tags + RAILWAY_TAGS + aeroway_tags, + draw_road_features( + highway_tags + railway_tags + aeroway_tags, ROAD_WIDTHS_AND_FEATURES, - Path("out") / "width.svg", + out_path / "width.svg", ) - road_features( + draw_road_features( highway_tags, PLACEMENT_FEATURES_1 + [{"highway": "none"}] + PLACEMENT_FEATURES_2, - Path("out") / "placement.svg", + out_path / "placement.svg", ) + draw_overlapped_ways(road_tags + railway_tags, out_path / "overlap.svg") diff --git a/map_machine/doc/icons.py b/map_machine/doc/icons.py index 7ec9df8..376059b 100644 --- a/map_machine/doc/icons.py +++ b/map_machine/doc/icons.py @@ -15,7 +15,6 @@ from map_machine.pictogram.icon import ( from map_machine.pictogram.icon_collection import IconCollection from map_machine.workspace import workspace - SKIP: bool = True diff --git a/map_machine/doc/preview.py b/map_machine/doc/preview.py index 2c5f5d7..1cb7035 100755 --- a/map_machine/doc/preview.py +++ b/map_machine/doc/preview.py @@ -9,19 +9,19 @@ from typing import Optional import numpy as np import svgwrite -from map_machine.geometry.boundary_box import BoundaryBox from map_machine.constructor import Constructor +from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.flinger import Flinger -from map_machine.pictogram.icon import ShapeExtractor -from map_machine.mapper import Map from map_machine.map_configuration import ( BuildingMode, DrawingMode, LabelMode, MapConfiguration, ) +from map_machine.mapper import Map from map_machine.osm.osm_getter import get_osm from map_machine.osm.osm_reader import OSMData +from map_machine.pictogram.icon import ShapeExtractor from map_machine.scheme import Scheme doc_path: Path = Path("doc") diff --git a/map_machine/doc/wiki.py b/map_machine/doc/wiki.py index 1a5ede4..516b432 100644 --- a/map_machine/doc/wiki.py +++ b/map_machine/doc/wiki.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Optional from map_machine.doc.collections import Collection + from map_machine.map_configuration import MapConfiguration from map_machine.osm.osm_reader import Tags from map_machine.pictogram.icon import Icon, ShapeExtractor diff --git a/map_machine/feature/road.py b/map_machine/feature/road.py index cd8236f..b2e1e2e 100644 --- a/map_machine/feature/road.py +++ b/map_machine/feature/road.py @@ -395,6 +395,13 @@ class Road(Tagged): except ValueError: pass + if "placement" in tags: + value: str = tags["placement"] + if ":" in value and len(parts := value.split(":")) == 2: + _, lane_string = parts + if (lane_number := int(lane_string) - 1) >= len(self.lanes): + self.lanes += [Lane()] * (lane_number + 1 - len(self.lanes)) + if "width:lanes" in tags: try: widths: List[float] = list( diff --git a/map_machine/map_configuration.py b/map_machine/map_configuration.py index 4727652..9161391 100644 --- a/map_machine/map_configuration.py +++ b/map_machine/map_configuration.py @@ -57,6 +57,7 @@ class MapConfiguration: draw_roofs: bool = True use_building_colors: bool = False show_overlapped: bool = False + credit: Optional[str] = "© OpenStreetMap contributors" @classmethod def from_options( diff --git a/map_machine/mapper.py b/map_machine/mapper.py index a301e59..f603917 100644 --- a/map_machine/mapper.py +++ b/map_machine/mapper.py @@ -19,6 +19,7 @@ from map_machine.constructor import Constructor from map_machine.drawing import draw_text from map_machine.feature.building import Building, draw_walls, BUILDING_SCALE from map_machine.feature.road import Intersection, Road, RoadPart +from map_machine.figure import StyledFigure from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.flinger import Flinger from map_machine.geometry.vector import Segment @@ -34,7 +35,7 @@ from map_machine.workspace import workspace __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -OPENSTREETMAP_CREDIT: str = "© OpenStreetMap contributors" +ROAD_PRIORITY: float = 40.0 class Map: @@ -63,7 +64,16 @@ class Map: ) logging.info("Drawing ways...") - for figure in constructor.get_sorted_figures(): + figures: list[StyledFigure] = constructor.get_sorted_figures() + + top_figures: list[StyledFigure] = [ + x for x in figures if x.line_style.priority >= ROAD_PRIORITY + ] + bottom_figures: list[StyledFigure] = [ + x for x in figures if x.line_style.priority < ROAD_PRIORITY + ] + + for figure in bottom_figures: path_commands: str = figure.get_path(self.flinger) if path_commands: path: SVGPath = SVGPath(d=path_commands) @@ -72,6 +82,13 @@ class Map: constructor.roads.draw(self.svg, self.flinger) + for figure in top_figures: + path_commands: str = figure.get_path(self.flinger) + if path_commands: + path: SVGPath = SVGPath(d=path_commands) + path.update(figure.line_style.style) + self.svg.add(path) + for tree in constructor.trees: tree.draw(self.svg, self.flinger, self.scheme) for crater in constructor.craters: @@ -207,13 +224,17 @@ class Map: text_color: Color = Color("#888888") outline_color: Color = Color("#FFFFFF") - for text, point in ( - ( - f"Data: {OPENSTREETMAP_CREDIT}", + credit_list: list[tuple[str, tuple[float, float]]] = [ + (f"Rendering: {__project__}", (right_margin, bottom_margin)) + ] + if self.configuration.credit: + data_credit: tuple[str, tuple[float, float]] = ( + f"Data: {self.configuration.credit}", (right_margin, bottom_margin + font_size + vertical_spacing), - ), - (f"Rendering: {__project__}", (right_margin, bottom_margin)), - ): + ) + credit_list.append(data_credit) + + for text, point in credit_list: for stroke_width, stroke, opacity in ( (3.0, outline_color, 0.7), (1.0, None, 1.0), @@ -255,12 +276,19 @@ def render_map(arguments: argparse.Namespace) -> None: if arguments.boundary_box: boundary_box = BoundaryBox.from_text(arguments.boundary_box) + elif arguments.coordinates and arguments.size: - coordinates: np.ndarray = np.array( - list(map(float, arguments.coordinates.split(","))) - ) - if len(coordinates) != 2: - fatal("Wrong number of coordinates.") + coordinates: Optional[np.ndarray] = None + + for delimiter in ",", "/": + if delimiter in arguments.coordinates: + coordinates = np.array( + list(map(float, arguments.coordinates.split(delimiter))) + ) + + if coordinates is None or len(coordinates) != 2: + fatal("Wrong coordinates format.") + width, height = np.array(list(map(float, arguments.size.split(",")))) boundary_box = BoundaryBox.from_coordinates( coordinates, configuration.zoom_level, width, height diff --git a/map_machine/osm/tags.py b/map_machine/osm/tags.py new file mode 100644 index 0000000..effa0e6 --- /dev/null +++ b/map_machine/osm/tags.py @@ -0,0 +1,41 @@ +ROAD_VALUES: list[str] = [ + "motorway", + "trunk", + "primary", + "secondary", + "tertiary", + "unclassified", + "residential", + "service", +] +HIGHWAY_VALUES: list[str] = ROAD_VALUES + [ + "service_minor", + "road", + "pedestrian", + "living_street", + "bridleway", + "cycleway", + "footway", + "steps", + "path", + "track", + "raceway", +] +AEROWAY_VALUES: list[str] = [ + "runway", + "taxiway", +] +RAILWAY_VALUES: list[str] = [ + "rail", + "subway", + "light_rail", + "monorail", + "narrow_gauge", + "tram", + "funicular", + "miniature", + "preserved", + "construction", + "disused", + "abandoned", +] diff --git a/map_machine/scheme/default.yml b/map_machine/scheme/default.yml index 77ec7c2..1ebada6 100644 --- a/map_machine/scheme/default.yml +++ b/map_machine/scheme/default.yml @@ -1949,7 +1949,7 @@ node_icons: - tags: {bus: "yes"} add_shapes: [bus] - - tags: {motocar: "yes"} + - tags: {motorcar: "yes"} add_shapes: [car] - tags: {car: "yes"} add_shapes: [car] @@ -2574,10 +2574,24 @@ ways: stroke: "#BBBBBB" priority: 42.0 - tags: {railway: construction} + style: + stroke-width: 2.0 + stroke: "#000000" + stroke-dasharray: 6,3 + opacity: 0.3 + priority: 42.0 + - tags: {railway: disused} style: stroke-width: 3.0 stroke: "#000000" - stroke-dasharray: 3,3 + stroke-dasharray: 6,6 + opacity: 0.3 + priority: 42.0 + - tags: {railway: abandoned} + style: + stroke-width: 3.0 + stroke: "#000000" + stroke-dasharray: 6,9 opacity: 0.3 priority: 42.0 diff --git a/map_machine/ui/cli.py b/map_machine/ui/cli.py index b785808..d10ffff 100644 --- a/map_machine/ui/cli.py +++ b/map_machine/ui/cli.py @@ -2,7 +2,6 @@ Command-line user interface. """ import argparse -import sys from typing import Dict, List from map_machine import __version__ @@ -12,9 +11,6 @@ from map_machine.osm.osm_reader import STAGES_OF_DECAY __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -BOXES: str = " ▏▎▍▌▋▊▉" -BOXES_LENGTH: int = len(BOXES) - COMMAND_LINES: Dict[str, List[str]] = { "render": ["render", "-b", "10.000,20.000,10.001,20.001"], "render_with_tooltips": [ @@ -392,36 +388,3 @@ def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None: 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( - number: int, total: int, length: int = 20, step: int = 1000, text: str = "" -) -> 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 - """ - if number == -1: - sys.stdout.write(f"100 % {length * '█'}▏{text}\n") - elif number % step == 0: - 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)] - sys.stdout.write( - f"{str(int(int(ratio * 1000.0) / 10.0)):>3} % " - f"{fill_length * '█'}{box}" - f"{int(length - fill_length - 1) * ' '}▏{text}\n\033[F" - ) diff --git a/tests/test_icons.py b/tests/test_icons.py index 82e29c9..723b490 100644 --- a/tests/test_icons.py +++ b/tests/test_icons.py @@ -1,13 +1,17 @@ """ Test icon generation for nodes. + +Tests check that for the given node described by tags, Map Machine generates +expected icons with expected colors. """ -from typing import Dict, List, Set, Tuple +from typing import List, Set, Tuple from pathlib import Path from typing import Optional from colour import Color +from map_machine.osm.osm_reader import Tags from map_machine.pictogram.icon import IconSet, ShapeSpecification, Icon from map_machine.pictogram.icon_collection import IconCollection from tests import SCHEME, SHAPE_EXTRACTOR, workspace @@ -43,7 +47,7 @@ def test_icons_by_name() -> None: assert (path / "LICENSE").is_file() -def get_icon(tags: Dict[str, str]) -> IconSet: +def get_icon(tags: Tags) -> IconSet: """Construct icon from tags.""" processed: Set[str] = set() icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed) @@ -71,11 +75,13 @@ def test_no_icons_but_color() -> None: def check_icon_set( - icon: IconSet, + tags: Tags, main_specification: List[Tuple[str, Optional[Color]]], extra_specifications: List[List[Tuple[str, Optional[Color]]]], ) -> None: """Check icon set using simple specification.""" + icon: IconSet = get_icon(tags) + if not main_specification: assert icon.main_icon.is_default() else: @@ -104,17 +110,17 @@ def test_icon() -> None: Tags that should be visualized with single main icon and without extra icons. """ - icon: IconSet = get_icon({"natural": "tree"}) - check_icon_set(icon, [("tree", Color("#98AC64"))], []) + check_icon_set({"natural": "tree"}, [("tree", Color("#98AC64"))], []) def test_icon_1_extra() -> None: """ Tags that should be visualized with single main icon and single extra icon. """ - icon: IconSet = get_icon({"barrier": "gate", "access": "private"}) check_icon_set( - icon, [("gate", DEFAULT_COLOR)], [[("lock_with_keyhole", EXTRA_COLOR)]] + {"barrier": "gate", "access": "private"}, + [("gate", DEFAULT_COLOR)], + [[("lock_with_keyhole", EXTRA_COLOR)]], ) @@ -122,11 +128,8 @@ def test_icon_2_extra() -> None: """ Tags that should be visualized with single main icon and two extra icons. """ - icon: IconSet = get_icon( - {"barrier": "gate", "access": "private", "bicycle": "yes"} - ) check_icon_set( - icon, + {"barrier": "gate", "access": "private", "bicycle": "yes"}, [("gate", DEFAULT_COLOR)], [ [("bicycle", EXTRA_COLOR)], @@ -139,17 +142,17 @@ def test_no_icon_1_extra() -> None: """ Tags that should be visualized with default main icon and single extra icon. """ - icon: IconSet = get_icon({"access": "private"}) - check_icon_set(icon, [], [[("lock_with_keyhole", EXTRA_COLOR)]]) + check_icon_set( + {"access": "private"}, [], [[("lock_with_keyhole", EXTRA_COLOR)]] + ) def test_no_icon_2_extra() -> None: """ Tags that should be visualized with default main icon and two extra icons. """ - icon: IconSet = get_icon({"access": "private", "bicycle": "yes"}) check_icon_set( - icon, + {"access": "private", "bicycle": "yes"}, [], [[("bicycle", EXTRA_COLOR)], [("lock_with_keyhole", EXTRA_COLOR)]], ) @@ -159,9 +162,8 @@ def test_icon_regex() -> None: """ Tags that should be visualized with default main icon and single extra icon. """ - icon: IconSet = get_icon({"traffic_sign": "maxspeed", "maxspeed": "42"}) check_icon_set( - icon, + {"traffic_sign": "maxspeed", "maxspeed": "42"}, [("circle_11", DEFAULT_COLOR), ("digit_4", WHITE), ("digit_2", WHITE)], [], ) @@ -169,22 +171,22 @@ def test_icon_regex() -> None: def test_vending_machine() -> None: """ - Check that specific vending machines doesn't render with generic icon. + Check that specific vending machines aren't rendered with generic icon. See https://github.com/enzet/map-machine/issues/132 """ check_icon_set( - get_icon({"amenity": "vending_machine"}), + {"amenity": "vending_machine"}, [("vending_machine", DEFAULT_COLOR)], [], ) check_icon_set( - get_icon({"amenity": "vending_machine", "vending": "drinks"}), + {"amenity": "vending_machine", "vending": "drinks"}, [("vending_bottle", DEFAULT_COLOR)], [], ) check_icon_set( - get_icon({"vending": "drinks"}), + {"vending": "drinks"}, [("vending_bottle", DEFAULT_COLOR)], [], ) diff --git a/tests/test_ways.py b/tests/test_ways.py index 3708c24..25a69bb 100644 --- a/tests/test_ways.py +++ b/tests/test_ways.py @@ -1,15 +1,25 @@ +""" +Test map generation for ways. + +Tests check that for the given ways described by tags, Map Machine generates +expected figures in the expected order. +""" import numpy as np +from map_machine.constructor import Constructor from map_machine.figure import Figure from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.flinger import Flinger from map_machine.map_configuration import MapConfiguration -from map_machine.constructor import Constructor -from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode +from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags from tests import SCHEME, SHAPE_EXTRACTOR def get_constructor(osm_data: OSMData) -> Constructor: + """ + Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level + 18. + """ flinger: Flinger = Flinger( BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length ) @@ -20,24 +30,26 @@ def get_constructor(osm_data: OSMData) -> Constructor: return constructor +def create_way(osm_data: OSMData, tags: Tags, index: int) -> None: + """Create simple OSM way with two arbitrary nodes.""" + nodes: list[OSMNode] = [ + OSMNode({}, 1, np.array((-0.01, -0.01))), + OSMNode({}, 2, np.array((0.01, 0.01))), + ] + for node in nodes: + osm_data.add_node(node) + osm_data.add_way(OSMWay(tags, index, nodes)) + + def test_river_and_wood() -> None: """ Check that river is above the wood. See https://github.com/enzet/map-machine/issues/126 """ - nodes_1: list[OSMNode] = [ - OSMNode({}, 1, np.array((-0.01, -0.01))), - OSMNode({}, 2, np.array((0.01, 0.01))), - ] - nodes_2: list[OSMNode] = [ - OSMNode({}, 3, np.array((-0.01, -0.01))), - OSMNode({}, 4, np.array((0.01, 0.01))), - ] - osm_data: OSMData = OSMData() - osm_data.add_way(OSMWay({"natural": "wood"}, 1, nodes_1)) - osm_data.add_way(OSMWay({"waterway": "river"}, 2, nodes_2)) + create_way(osm_data, {"natural": "wood"}, 1) + create_way(osm_data, {"waterway": "river"}, 2) figures: list[Figure] = get_constructor(osm_data).get_sorted_figures() @@ -46,6 +58,19 @@ def test_river_and_wood() -> None: assert figures[1].tags["waterway"] == "river" +def test_placement_and_lanes() -> None: + """ + Check that `placement` tag is processed correctly when `lanes` tag is not + specified. + + See https://github.com/enzet/map-machine/issues/128 + """ + osm_data: OSMData = OSMData() + create_way(osm_data, {"highway": "motorway", "placement": "right_of:2"}, 1) + + get_constructor(osm_data) + + def test_empty_ways() -> None: """Ways without nodes.""" osm_data: OSMData = OSMData()