diff --git a/doc/readme.moi b/doc/readme.moi index 304db60..338fd18 100644 --- a/doc/readme.moi +++ b/doc/readme.moi @@ -36,7 +36,7 @@ Röntgen features\: \3 {Isometric building shapes} {levels} -Isometric shapes for walls and shade in proportion to \osm {building:levels}, \osm {building:min_level}, \osm {height} and \osm {min_height} values. +With \m {--buildings isometric} or \m {--buildings isometric-no-parts} (not set by default), buildings are drawn using isometric shapes for walls and shade in proportion to \osm {building:levels}, \osm {building:min_level}, \osm {height} and \osm {min_height} values. \image {doc/buildings.png} {3D buildings} @@ -265,5 +265,5 @@ To enable / disable Röntgen map paint style go to \kbd {View} → \kbd {Map Pai Example of using Röntgen icons on top of Mapnik style. Map Paint Styles\: \list - {✓ Mapnik} + {✓ Mapnik (true)} {✓ Röntgen} diff --git a/roentgen/boundary_box.py b/roentgen/boundary_box.py index 9a9d581..aa3793e 100644 --- a/roentgen/boundary_box.py +++ b/roentgen/boundary_box.py @@ -7,6 +7,9 @@ from dataclasses import dataclass import numpy as np +__author__ = "Sergey Vartanov" +__email__ = "me@enzet.ru" + LATITUDE_MAX_DIFFERENCE: float = 0.5 LONGITUDE_MAX_DIFFERENCE: float = 0.5 diff --git a/roentgen/color.py b/roentgen/color.py index 7f563f9..e9a8840 100644 --- a/roentgen/color.py +++ b/roentgen/color.py @@ -1,7 +1,7 @@ """ Color utility. """ -from typing import Any, List +from typing import Any 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/roentgen/constructor.py b/roentgen/constructor.py index 1aff9ea..a26f34e 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -13,6 +13,7 @@ from roentgen import ui from roentgen.color import get_gradient_color from roentgen.figure import Building, DirectionSector, Road, StyledFigure, Tree from roentgen.flinger import Flinger +from roentgen.map_configuration import DrawingMode, MapConfiguration # fmt: off from roentgen.icon import ( @@ -21,9 +22,8 @@ from roentgen.icon import ( from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay from roentgen.point import Point from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme -from roentgen.ui import AUTHOR_MODE, BuildingMode, TIME_MODE +from roentgen.ui import BuildingMode from roentgen.util import MinMax - # fmt: on __author__ = "Sergey Vartanov" @@ -113,7 +113,7 @@ def glue(ways: list[OSMWay]) -> list[list[OSMNode]]: return result -def is_cycle(nodes) -> bool: +def is_cycle(nodes: list[OSMNode]) -> bool: """Is way a cycle way or an area boundary.""" return nodes[0] == nodes[-1] @@ -129,22 +129,22 @@ class Constructor: flinger: Flinger, scheme: Scheme, icon_extractor: ShapeExtractor, - options, + configuration: MapConfiguration, ) -> None: self.osm_data: OSMData = osm_data self.flinger: Flinger = flinger self.scheme: Scheme = scheme self.icon_extractor = icon_extractor - self.options = options + self.configuration = configuration - if options.level: - if options.level == "overground": + if self.configuration.level: + if self.configuration.level == "overground": self.check_level = check_level_overground - elif options.level == "underground": + elif self.configuration.level == "underground": self.check_level = lambda x: not check_level_overground(x) else: self.check_level = lambda x: not check_level_number( - x, float(options.level) + x, float(self.configuration.level) ) else: self.check_level = lambda x: True @@ -203,10 +203,10 @@ class Constructor: return center_point, center_coordinates = line_center(outers[0], self.flinger) - if self.options.mode in [AUTHOR_MODE, TIME_MODE]: + if self.configuration.is_wireframe(): color: Color - if self.options.mode == AUTHOR_MODE: - color = get_user_color(line.user, self.options.seed) + if self.configuration.drawing_mode == DrawingMode.AUTHOR: + color = get_user_color(line.user, self.configuration.seed) else: # self.mode == TIME_MODE color = get_time_color(line.timestamp, self.osm_data.time) self.draw_special_mode(inners, line, outers, color) @@ -215,7 +215,7 @@ class Constructor: if not line.tags: return - building_mode: BuildingMode = BuildingMode(self.options.buildings) + building_mode: BuildingMode = self.configuration.building_mode if "building" in line.tags or ( building_mode == BuildingMode.ISOMETRIC and "building:part" in line.tags @@ -352,13 +352,13 @@ class Constructor: icon_set: IconSet draw_outline: bool = True - if self.options.mode in [TIME_MODE, AUTHOR_MODE]: + if self.configuration.is_wireframe(): if not tags: return color: Color = DEFAULT_COLOR - if self.options.mode == AUTHOR_MODE: - color = get_user_color(node.user, self.options.seed) - if self.options.mode == TIME_MODE: + if self.configuration.drawing_mode == DrawingMode.AUTHOR: + color = get_user_color(node.user, self.configuration.seed) + if self.configuration.drawing_mode == DrawingMode.TIME: color = get_time_color(node.timestamp, self.osm_data.time) dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID) icon_set = IconSet( diff --git a/roentgen/drawing.py b/roentgen/drawing.py index 3853586..9d058d2 100644 --- a/roentgen/drawing.py +++ b/roentgen/drawing.py @@ -3,7 +3,7 @@ Drawing utility. """ from dataclasses import dataclass from pathlib import Path -from typing import List, Optional, Union +from typing import Optional, Union import cairo import numpy as np @@ -73,7 +73,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 @@ -111,7 +111,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: @@ -159,7 +159,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])) diff --git a/roentgen/element.py b/roentgen/element.py index 99b1a60..058d27f 100644 --- a/roentgen/element.py +++ b/roentgen/element.py @@ -1,8 +1,8 @@ """ Drawing separate map elements. """ +import argparse import logging -import sys from pathlib import Path import numpy as np @@ -13,15 +13,18 @@ from roentgen.point import Point from roentgen.scheme import LineStyle, Scheme from roentgen.workspace import workspace +__author__ = "Sergey Vartanov" +__email__ = "me@enzet.ru" -def draw_element(options) -> None: + +def draw_element(options: argparse.Namespace) -> None: """Draw single node, line, or area.""" if options.node: target: str = "node" tags_description = options.node else: # Not implemented yet. - sys.exit(1) + exit(1) tags: dict[str, str] = dict( [x.split("=") for x in tags_description.split(",")] diff --git a/roentgen/figure.py b/roentgen/figure.py index 69930a2..37a080b 100644 --- a/roentgen/figure.py +++ b/roentgen/figure.py @@ -1,7 +1,7 @@ """ Figures displayed on the map. """ -from typing import Any, Dict, List, Optional +from typing import Any, Optional import numpy as np from colour import Color @@ -25,15 +25,15 @@ 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__() - self.tags: Dict[str, str] = tags - self.inners: List[List[OSMNode]] = [] - self.outers: List[List[OSMNode]] = [] + self.tags: dict[str, str] = tags + self.inners: list[list[OSMNode]] = [] + self.outers: list[list[OSMNode]] = [] for inner_nodes in inners: self.inners.append(make_clockwise(inner_nodes)) @@ -67,15 +67,15 @@ 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, } @@ -195,9 +195,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) @@ -211,16 +211,16 @@ class Road(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]], matcher: RoadMatcher, ) -> None: super().__init__(tags, inners, outers) self.matcher: RoadMatcher = matcher self.width: Optional[float] = None - self.lanes: List[Lane] = [] + self.lanes: list[Lane] = [] if "lanes" in tags: try: @@ -359,7 +359,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. @@ -374,7 +374,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. @@ -383,7 +383,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. @@ -392,7 +392,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.array, flinger: Flinger) -> str: +def get_path(nodes: list[OSMNode], shift: np.array, flinger: Flinger) -> str: """Construct SVG path commands from nodes.""" path: str = "" prev_node: Optional[OSMNode] = None diff --git a/roentgen/grid.py b/roentgen/grid.py index 8d55018..3589add 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -34,6 +34,7 @@ class IconCollection: background_color: Color = Color("white"), color: Color = Color("black"), add_unused: bool = False, + add_all: bool = False, ) -> "IconCollection": """ Collect all possible icon combinations in grid. @@ -44,6 +45,7 @@ class IconCollection: :param color: icon color :param add_unused: create icons from shapes that have no corresponding tags + :param add_all: create icons from all possible shapes including parts """ icons: list[Icon] = [] @@ -111,6 +113,13 @@ class IconCollection: icon.recolor(color) icons.append(icon) + if add_all: + for shape_id in extractor.shapes.keys(): + shape: Shape = extractor.get_shape(shape_id) + icon: Icon = Icon([ShapeSpecification(shape)]) + icon.recolor(color) + icons.append(icon) + return cls(icons) def draw_icons( @@ -204,8 +213,9 @@ def draw_icons() -> None: extractor: ShapeExtractor = ShapeExtractor( workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH ) - collection: IconCollection = IconCollection.from_scheme(scheme, extractor) - + collection: IconCollection = IconCollection.from_scheme( + scheme, extractor, add_all=True + ) icon_grid_path: Path = workspace.get_icon_grid_path() collection.draw_grid(icon_grid_path) logging.info(f"Icon grid is written to {icon_grid_path}.") diff --git a/roentgen/icon.py b/roentgen/icon.py index 7a6246e..2ca4fd9 100644 --- a/roentgen/icon.py +++ b/roentgen/icon.py @@ -322,7 +322,7 @@ class ShapeSpecification: def draw( self, - svg, + svg: Drawing, point: np.array, tags: dict[str, Any] = None, outline: bool = False, @@ -372,7 +372,7 @@ class ShapeSpecification: and np.allclose(self.offset, other.offset) ) - def __lt__(self, other) -> bool: + def __lt__(self, other: "ShapeSpecification") -> bool: return self.shape.id_ < other.shape.id_ @@ -471,12 +471,12 @@ class Icon: """Add shape specifications to the icon.""" self.shape_specifications += specifications - def __eq__(self, other) -> bool: + def __eq__(self, other: "Icon") -> bool: return sorted(self.shape_specifications) == sorted( other.shape_specifications ) - def __lt__(self, other) -> bool: + def __lt__(self, other: "Icon") -> bool: return "".join( [x.shape.id_ for x in self.shape_specifications] ) < "".join([x.shape.id_ for x in other.shape_specifications]) diff --git a/roentgen/mapcss.py b/roentgen/mapcss.py index 5b8c320..8ce46dc 100644 --- a/roentgen/mapcss.py +++ b/roentgen/mapcss.py @@ -1,6 +1,7 @@ """ MapCSS scheme creation. """ +import argparse import logging from pathlib import Path from typing import Optional, TextIO @@ -175,7 +176,7 @@ class MapCSSWriter: ) -def ui(options) -> None: +def ui(options: argparse.Namespace) -> None: """Write MapCSS 0.2 scheme.""" directory: Path = workspace.get_mapcss_path() icons_with_outline_path: Path = workspace.get_mapcss_icons_path() diff --git a/roentgen/mapper.py b/roentgen/mapper.py index 6dee615..585bfea 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -1,6 +1,7 @@ """ Simple OpenStreetMap renderer. """ +import argparse import logging from pathlib import Path from typing import Any, Iterator @@ -17,12 +18,13 @@ from roentgen.constructor import Constructor from roentgen.figure import Road from roentgen.flinger import Flinger from roentgen.icon import ShapeExtractor +from roentgen.map_configuration import LabelMode, MapConfiguration from roentgen.osm_getter import NetworkError, get_osm from roentgen.osm_reader import OSMData, OSMNode, OSMReader, OverpassReader from roentgen.point import Occupied from roentgen.road import Intersection, RoadPart from roentgen.scheme import Scheme -from roentgen.ui import AUTHOR_MODE, BuildingMode, TIME_MODE, progress_bar +from roentgen.ui import BuildingMode, progress_bar from roentgen.workspace import workspace __author__ = "Sergey Vartanov" @@ -35,15 +37,19 @@ class Map: """ def __init__( - self, flinger: Flinger, svg: svgwrite.Drawing, scheme: Scheme, options + self, + flinger: Flinger, + svg: svgwrite.Drawing, + scheme: Scheme, + configuration: MapConfiguration, ) -> None: self.flinger: Flinger = flinger self.svg: svgwrite.Drawing = svg self.scheme: Scheme = scheme - self.options = options + self.configuration = configuration self.background_color: Color = self.scheme.get_color("background_color") - if self.options.mode in [AUTHOR_MODE, TIME_MODE]: + if self.configuration.is_wireframe(): self.background_color: Color = Color("#111111") def draw(self, constructor: Constructor) -> None: @@ -80,11 +86,13 @@ class Map: # All other points - if self.options.overlap == 0: + if self.configuration.overlap == 0: occupied = None else: occupied = Occupied( - self.flinger.size[0], self.flinger.size[1], self.options.overlap + self.flinger.size[0], + self.flinger.size[1], + self.configuration.overlap, ) nodes = sorted(constructor.points, key=lambda x: -x.priority) @@ -105,18 +113,18 @@ class Map: steps * 2 + index, steps * 3, step=10, text="Drawing texts" ) if ( - self.options.mode not in [TIME_MODE, AUTHOR_MODE] - and self.options.label_mode != "no" + not self.configuration.is_wireframe() + and self.configuration.label_mode != LabelMode.NO ): - point.draw_texts(self.svg, occupied, self.options.label_mode) + point.draw_texts( + self.svg, occupied, self.configuration.label_mode + ) progress_bar(-1, len(nodes), step=10, text="Drawing nodes") def draw_buildings(self, constructor: Constructor) -> None: """Draw buildings: shade, walls, and roof.""" - building_mode: BuildingMode = BuildingMode(self.options.buildings) - - if building_mode == BuildingMode.FLAT: + if self.configuration.building_mode == BuildingMode.FLAT: for building in constructor.buildings: building.draw(self.svg, self.flinger) return @@ -197,12 +205,14 @@ class Map: intersection.draw(self.svg, True) -def ui(options) -> None: +def ui(options: argparse.Namespace) -> None: """ Röntgen entry point. :param options: command-line arguments """ + configuration: MapConfiguration = MapConfiguration.from_options(options) + if not options.boundary_box and not options.input_file_name: logging.fatal("Specify either --boundary-box, or --input.") exit(1) @@ -241,8 +251,7 @@ def ui(options) -> None: osm_data = reader.osm_data view_box = boundary_box else: - is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE] - osm_reader = OSMReader(is_full=is_full) + osm_reader = OSMReader(is_full=configuration.is_wireframe()) for file_name in input_file_names: if not file_name.is_file(): @@ -273,11 +282,13 @@ def ui(options) -> None: flinger=flinger, scheme=scheme, icon_extractor=icon_extractor, - options=options, + configuration=configuration, ) constructor.construct() - painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme, options=options) + painter: Map = Map( + flinger=flinger, svg=svg, scheme=scheme, configuration=configuration + ) painter.draw(constructor) logging.info(f"Writing output SVG to {options.output_file_name}...") diff --git a/roentgen/point.py b/roentgen/point.py index 83983cb..b88b554 100644 --- a/roentgen/point.py +++ b/roentgen/point.py @@ -8,6 +8,7 @@ import svgwrite from colour import Color from roentgen.icon import Icon, IconSet +from roentgen.map_configuration import LabelMode from roentgen.osm_reader import Tagged from roentgen.text import Label @@ -161,14 +162,14 @@ class Point(Tagged): self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None, - label_mode: str = "main", + label_mode: LabelMode = LabelMode.MAIN, ) -> None: """Draw all labels.""" labels: list[Label] - if label_mode == "main": + if label_mode == LabelMode.MAIN: labels = self.labels[:1] - elif label_mode == "all": + elif label_mode == LabelMode.ALL: labels = self.labels else: return diff --git a/roentgen/server.py b/roentgen/server.py index 182198b..76aca53 100644 --- a/roentgen/server.py +++ b/roentgen/server.py @@ -1,6 +1,7 @@ """ Röntgen tile server for slippy maps. """ +import argparse import logging from http.server import HTTPServer, SimpleHTTPRequestHandler from pathlib import Path @@ -62,7 +63,7 @@ class _Handler(SimpleHTTPRequestHandler): return -def ui(options) -> None: +def ui(options: argparse.Namespace) -> None: """Command-line interface for tile server.""" server: Optional[HTTPServer] = None try: diff --git a/roentgen/tile.py b/roentgen/tile.py index 0460232..5780971 100644 --- a/roentgen/tile.py +++ b/roentgen/tile.py @@ -3,6 +3,7 @@ Tile generation. See https://wiki.openstreetmap.org/wiki/Tiles """ +import argparse import logging import sys from dataclasses import dataclass @@ -18,6 +19,7 @@ from roentgen.constructor import Constructor from roentgen.flinger import Flinger from roentgen.icon import ShapeExtractor from roentgen.mapper import Map +from roentgen.map_configuration import MapConfiguration from roentgen.osm_getter import NetworkError, get_osm from roentgen.osm_reader import OSMData, OSMReader from roentgen.scheme import Scheme @@ -115,23 +117,31 @@ class Tile: f"https://tile.openstreetmap.org/{self.scale}/{self.x}/{self.y}.png" ) - def draw(self, directory_name: Path, cache_path: Path, options) -> None: + def draw( + self, + directory_name: Path, + cache_path: Path, + configuration: MapConfiguration, + ) -> None: """ Draw tile to SVG and PNG files. :param directory_name: output directory to storing tiles :param cache_path: directory to store SVG and PNG tiles - :param options: drawing configuration + :param configuration: drawing configuration """ try: osm_data: OSMData = self.load_osm_data(cache_path) except NetworkError as e: raise NetworkError(f"Map is not loaded. {e.message}") - self.draw_with_osm_data(osm_data, directory_name, options) + self.draw_with_osm_data(osm_data, directory_name, configuration) def draw_with_osm_data( - self, osm_data: OSMData, directory_name: Path, options + self, + osm_data: OSMData, + directory_name: Path, + configuration: MapConfiguration, ) -> None: """Draw SVG and PNG tile using OpenStreetMap data.""" top, left = self.get_coordinates() @@ -154,12 +164,12 @@ class Tile: ) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) constructor: Constructor = Constructor( - osm_data, flinger, scheme, icon_extractor, options + osm_data, flinger, scheme, icon_extractor, configuration ) constructor.construct() painter: Map = Map( - flinger=flinger, svg=svg, scheme=scheme, options=options + flinger=flinger, svg=svg, scheme=scheme, configuration=configuration ) painter.draw(constructor) @@ -216,7 +226,7 @@ class Tiles: return cls(tiles, tile_1, tile_2, scale, extended_boundary_box) def draw_separately( - self, directory: Path, cache_path: Path, options + self, directory: Path, cache_path: Path, configuration: MapConfiguration ) -> None: """ Draw set of tiles as SVG file separately and rasterize them into a set @@ -224,7 +234,7 @@ class Tiles: :param directory: directory for tiles :param cache_path: directory for temporary OSM files - :param options: drawing configuration + :param configuration: drawing configuration """ cache_file_path: Path = ( cache_path / f"{self.boundary_box.get_format()}.osm" @@ -235,7 +245,7 @@ class Tiles: for tile in self.tiles: file_path: Path = tile.get_file_name(directory) if not file_path.exists(): - tile.draw_with_osm_data(osm_data, directory, options) + tile.draw_with_osm_data(osm_data, directory, configuration) else: logging.debug(f"File {file_path} already exists.") @@ -253,19 +263,21 @@ class Tiles: """Check whether all tiles are drawn.""" return all(x.exists(directory_name) for x in self.tiles) - def draw(self, directory: Path, cache_path: Path, options) -> None: + def draw( + self, directory: Path, cache_path: Path, configuration: MapConfiguration + ) -> None: """ Draw one PNG image with all tiles and split it into a set of separate PNG file with Pillow. :param directory: directory for tiles :param cache_path: directory for temporary OSM files - :param options: drawing configuration + :param configuration: drawing configuration """ if self.tiles_exist(directory): return - self.draw_image(cache_path, options) + self.draw_image(cache_path, configuration) input_path: Path = self.get_file_path(cache_path).with_suffix(".png") with input_path.open("rb") as input_file: @@ -290,12 +302,14 @@ class Tiles: """Get path of the output SVG file.""" return cache_path / f"{self.boundary_box.get_format()}_{self.scale}.svg" - def draw_image(self, cache_path: Path, options) -> None: + def draw_image( + self, cache_path: Path, configuration: MapConfiguration + ) -> None: """ Draw all tiles as one picture. :param cache_path: directory for temporary SVG file and OSM files - :param options: drawing configuration + :param configuration: drawing configuration """ output_path: Path = self.get_file_path(cache_path) @@ -319,16 +333,14 @@ class Tiles: ) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) constructor: Constructor = Constructor( - osm_data, flinger, scheme, extractor, options=options + osm_data, flinger, scheme, extractor, configuration ) constructor.construct() svg: svgwrite.Drawing = svgwrite.Drawing( str(output_path), size=flinger.size ) - map_: Map = Map( - flinger=flinger, svg=svg, scheme=scheme, options=options - ) + map_: Map = Map(flinger, svg, scheme, configuration) map_.draw(constructor) logging.info(f"Writing output SVG {output_path}...") @@ -346,9 +358,10 @@ class Tiles: logging.debug(f"File {png_path} already exists.") -def ui(options) -> None: +def ui(options: argparse.Namespace) -> None: """Simple user interface for tile generation.""" directory: Path = workspace.get_tile_path() + configuration: MapConfiguration = MapConfiguration.from_options(options) if options.coordinates: coordinates: list[float] = list( @@ -356,13 +369,13 @@ def ui(options) -> None: ) tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale) try: - tile.draw(directory, Path(options.cache), options) + tile.draw(directory, Path(options.cache), configuration) except NetworkError as e: logging.fatal(e.message) elif options.tile: scale, x, y = map(int, options.tile.split("/")) tile: Tile = Tile(x, y, scale) - tile.draw(directory, Path(options.cache), options) + tile.draw(directory, Path(options.cache), configuration) elif options.boundary_box: boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text( options.boundary_box @@ -370,7 +383,7 @@ def ui(options) -> None: if boundary_box is None: sys.exit(1) tiles: Tiles = Tiles.from_boundary_box(boundary_box, options.scale) - tiles.draw(directory, Path(options.cache), options) + tiles.draw(directory, Path(options.cache), configuration) else: logging.fatal( "Specify either --coordinates, --boundary-box, or --tile." diff --git a/roentgen/ui.py b/roentgen/ui.py index 7011fe2..c9a44fe 100644 --- a/roentgen/ui.py +++ b/roentgen/ui.py @@ -4,29 +4,15 @@ Command-line user interface. import argparse import sys +from roentgen.map_configuration import BuildingMode, DrawingMode, LabelMode +from roentgen.osm_reader import STAGES_OF_DECAY + __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -from enum import Enum - -from roentgen.osm_reader import STAGES_OF_DECAY - BOXES: str = " ▏▎▍▌▋▊▉" BOXES_LENGTH: int = len(BOXES) -AUTHOR_MODE: str = "author" -TIME_MODE: str = "time" - - -class BuildingMode(Enum): - """ - Building drawing mode. - """ - - FLAT = "flat" - ISOMETRIC = "isometric" - ISOMETRIC_NO_PARTS = "isometric-no-parts" - def parse_options(args) -> argparse.Namespace: """Parse Röntgen command-line options.""" @@ -68,8 +54,9 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--mode", default="normal", - help="map drawing mode", metavar="", + choices=(x.value for x in DrawingMode), + help="map drawing mode: " + ", ".join(x.value for x in DrawingMode), ) parser.add_argument( "--overlap", @@ -81,10 +68,11 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None: ) parser.add_argument( "--labels", - help="label drawing mode: `no`, `main`, or `all`", dest="label_mode", default="main", metavar="", + choices=(x.value for x in LabelMode), + help="label drawing mode: " + ", ".join(x.value for x in LabelMode), ) parser.add_argument( "-s", @@ -99,6 +87,12 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None: default="overground", help="display only this floor level", ) + parser.add_argument( + "--seed", + default="", + help="seed for random", + metavar="", + ) def add_tile_arguments(parser: argparse.ArgumentParser) -> None: @@ -179,12 +173,6 @@ def add_render_arguments(parser: argparse.ArgumentParser) -> None: default="cache", metavar="", ) - parser.add_argument( - "--seed", - default="", - help="seed for random", - metavar="", - ) def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None: @@ -225,14 +213,13 @@ def progress_bar( :param text: short description """ if number == -1: - print(f"100 % {length * '█'}▏{text}") + 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)] - print( + sys.stdout.write( f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * '█'}{box}" - f"{int(length - fill_length - 1) * ' '}▏{text}" + f"{int(length - fill_length - 1) * ' '}▏{text}\n\033[F" ) - sys.stdout.write("\033[F")