"""Draw test nodes, ways, and relations.""" import logging from pathlib import Path from typing import Optional import numpy as np from svgwrite import Drawing from svgwrite.text import Text from map_machine.constructor import Constructor 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.mapper import Map from map_machine.osm.osm_reader import ( OSMData, OSMNode, OSMWay, Tags, OSMRelation, OSMMember, ) 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 workspace: Workspace = Workspace(Path("temp")) SCHEME: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH) SHAPE_EXTRACTOR: ShapeExtractor = ShapeExtractor( workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH ) DEFAULT_ZOOM: float = 18.0 ROAD_WIDTHS_AND_FEATURES: list[dict[str, str]] = [ {"width": "4"}, {"width": "8"}, {"width": "12"}, {"width": "16"}, {"bridge": "yes", "width": "4"}, {"bridge": "yes", "width": "8"}, {"tunnel": "yes", "width": "4"}, {"tunnel": "yes", "width": "8"}, {"ford": "yes", "width": "4"}, {"ford": "yes", "width": "8"}, {"embankment": "yes", "width": "4"}, {"embankment": "yes", "width": "8"}, ] ROAD_LANES_AND_FEATURES: list[dict[str, str]] = [ {"lanes": "1"}, {"lanes": "2"}, {"lanes": "3"}, {"lanes": "4"}, {"bridge": "yes", "lanes": "1"}, {"bridge": "yes", "lanes": "2"}, {"tunnel": "yes", "lanes": "1"}, {"tunnel": "yes", "lanes": "2"}, {"ford": "yes", "lanes": "1"}, {"ford": "yes", "lanes": "2"}, {"embankment": "yes", "lanes": "1"}, {"embankment": "yes", "lanes": "2"}, ] # See https://wiki.openstreetmap.org/wiki/Proposed_features/placement PLACEMENT_FEATURES_1: list[dict[str, str]] = [ {"lanes": "1"}, {"lanes": "2", "placement": "middle_of:1"}, {"lanes": "4", "placement": "middle_of:2"}, {"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 {"lanes": "3", "placement:forward": "left_of:1"}, {"lanes": "3", "placement": "transition"}, {"lanes": "4", "placement:backward": "middle_of:1"}, {"lanes": "3"}, ] class Grid: """Creating map with elements ordered in grid.""" 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.relation_id: int = 0 self.osm_data: OSMData = OSMData() self.texts: list[tuple[str, int, int]] = [] def add_node(self, tags: Tags, i: int, j: int) -> OSMNode: """Add OSM node to the grid.""" self.index += 1 node: OSMNode = OSMNode( tags, self.index, np.array((-i * self.y_step, j * self.x_step)), ) self.nodes[node] = (j, i) self.osm_data.add_node(node) self.max_j = max(self.max_j, j * self.x_step) self.max_i = max(self.max_i, i * self.y_step) return node def add_way(self, tags: Tags, nodes: list[OSMNode]) -> OSMWay: """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 return osm_way def add_relation(self, tags: Tags, members: list[OSMMember]) -> OSMRelation: """Connect objects on the gird with relations.""" osm_relation: OSMRelation = OSMRelation(tags, self.relation_id, members) self.osm_data.add_relation(osm_relation) self.relation_id += 1 return osm_relation def add_text(self, text: str, i: int, j: int) -> None: """Add simple text label to the grid.""" 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 * 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) 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. The goal is to show check priority. """ 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.""" grid: Grid = Grid() for i, type_ in enumerate(types): previous: Optional[OSMNode] = None for j in range(len(features) + 1): node: OSMNode = grid.add_node({}, i, j) if previous: tags: dict[str, str] = dict(type_) tags |= dict(features[j - 1]) grid.add_way(tags, [previous, node]) previous = node grid.draw(path) def draw_multipolygon(path: Path) -> None: """Draw simple multipolygon with one outer and one inner way.""" grid: Grid = Grid(y_step=0.0002) outer_node: OSMNode = grid.add_node({}, 0, 0) outer_nodes: list[OSMNode] = [ outer_node, grid.add_node({}, 0, 3), grid.add_node({}, 3, 3), grid.add_node({}, 3, 0), outer_node, ] inner_node: OSMNode = grid.add_node({}, 1, 1) inner_nodes: list[OSMNode] = [ inner_node, grid.add_node({}, 1, 2), grid.add_node({}, 2, 2), grid.add_node({}, 2, 1), inner_node, ] outer = grid.add_way({}, outer_nodes) inner = grid.add_way({}, inner_nodes) members: list[OSMMember] = [ OSMMember("way", outer.id_, "outer"), OSMMember("way", inner.id_, "inner"), ] grid.add_relation({"natural": "water", "type": "multipolygon"}, members) 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 ] draw_road_features( highway_tags, ROAD_LANES_AND_FEATURES, out_path / "lanes.svg" ) draw_road_features( highway_tags + railway_tags + aeroway_tags, ROAD_WIDTHS_AND_FEATURES, out_path / "width.svg", ) draw_road_features( highway_tags, PLACEMENT_FEATURES_1 + [{"highway": "none"}] + PLACEMENT_FEATURES_2, out_path / "placement.svg", ) draw_overlapped_ways(road_tags + railway_tags, out_path / "overlap.svg") draw_multipolygon(out_path / "multipolygon.svg")