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()