diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89ce2d0..a836f5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest flake8 black + pip install black flake8 pytest pip install -r requirements.txt pip install . - name: Test with pytest diff --git a/README.md b/README.md index 6b2347e..213e570 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ The central feature of the project is Röntgen icon set. It is a set of monochro All icons tend to support common design style, which is heavily inspired by [Maki](https://github.com/mapbox/maki), [Osmic](https://github.com/gmgeo/osmic), and [Temaki](https://github.com/ideditor/temaki). -Icons are used to visualize tags for nodes and areas. Unlike other renderers, Röntgen can use more than one icon to visualize an entity and use colors to visualize [`colour`](https://wiki.openstreetmap.org/wiki/Key:colour) value or other entity properties (like material or genus). +Icons are used to visualize tags for nodes and areas. Unlike other renderers, Röntgen can use more than one icon to visualize an entity and use colors to visualize [`colour`](https://wiki.openstreetmap.org/wiki/Key:colour) value or other entity properties (like [`material`](https://wiki.openstreetmap.org/wiki/Key:material) or [`genus`](https://wiki.openstreetmap.org/wiki/Key:genus)). ![Icons](doc/grid.png) @@ -217,7 +217,7 @@ Example: roentgen tile -b 2.361,48.871,2.368,48.875 ``` -will generate 36 PNG tiles at scale 18 from tile 18/132791/90164 all the way to 18/132796/90169 and two cached files `cache/2.360,48.869,2.370,48.877.svg` and `cache/2.360,48.869,2.370,48.877.png`. +will generate 36 PNG tiles at scale 18 from tile 18/132791/90164 all the way to 18/132796/90169 and two cached files `cache/2.360,48.869,2.370,48.877_18.svg` and `cache/2.360,48.869,2.370,48.877_18.png`. Tile server ----------- diff --git a/doc/readme.moi b/doc/readme.moi index bd0a8c2..26f9878 100644 --- a/doc/readme.moi +++ b/doc/readme.moi @@ -4,7 +4,7 @@ \b {Röntgen} (or \b {Roentgen} when ASCII is preferred) project consists of \list - {simple Python \ref {http://openstreetmap.org} {OpenStreetMap} renderer (see \ref {#usage} {usage}, \ref {#map-generation} {renderer documentation}),} + {simple Python \ref {http://openstreetmap.org} {OpenStreetMap} renderer and tile generator (see \ref {#usage-example} {usage}, \ref {#map-generation} {renderer documentation}, \ref {#tile-generation} {tile generation}),} {\ref {#icon-set} {set of CC-BY 4.0 icons} that can be used outside the project.} The idea behind the Röntgen project is to \b {show all the richness of the OpenStreetMap data}\: to have a possibility to \i {display any map feature} represented by OpenStreetMap data tags by means of colors, shapes, and icons. Röntgen is created for OpenStreetMap contributors\: to display all changes one made on the map even if they are small, and for users\: to dig down into the map and find every detail that was mapped. @@ -93,7 +93,7 @@ The central feature of the project is Röntgen icon set. It is a set of monochr All icons tend to support common design style, which is heavily inspired by \ref {https://github.com/mapbox/maki} {Maki}, \ref {https://github.com/gmgeo/osmic} {Osmic}, and \ref {https://github.com/ideditor/temaki} {Temaki}. -Icons are used to visualize tags for nodes and areas. Unlike other renderers, Röntgen can use more than one icon to visualize an entity and use colors to visualize \osm {colour} value or other entity properties (like material or genus). +Icons are used to visualize tags for nodes and areas. Unlike other renderers, Röntgen can use more than one icon to visualize an entity and use colors to visualize \osm {colour} value or other entity properties (like \osm {material} or \osm {genus}). \image {doc/grid.png} {Icons} @@ -230,7 +230,7 @@ Example\: \code {roentgen tile -b 2.361,48.871,2.368,48.875} {bash} -will generate 36 PNG tiles at scale 18 from tile 18/132791/90164 all the way to 18/132796/90169 and two cached files \m {cache/2.360,48.869,2.370,48.877.svg} and \m {cache/2.360,48.869,2.370,48.877.png}. +will generate 36 PNG tiles at scale 18 from tile 18/132791/90164 all the way to 18/132796/90169 and two cached files \m {cache/2.360,48.869,2.370,48.877_18.svg} and \m {cache/2.360,48.869,2.370,48.877_18.png}. \2 {Tile server} {tile-server} diff --git a/roentgen/boundary_box.py b/roentgen/boundary_box.py index 2342777..9a9d581 100644 --- a/roentgen/boundary_box.py +++ b/roentgen/boundary_box.py @@ -1,3 +1,6 @@ +""" +Rectangle that limit space on the map. +""" import logging import re from dataclasses import dataclass diff --git a/roentgen/constructor.py b/roentgen/constructor.py index 5cf441b..1aff9ea 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -21,7 +21,7 @@ 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, TIME_MODE +from roentgen.ui import AUTHOR_MODE, BuildingMode, TIME_MODE from roentgen.util import MinMax # fmt: on @@ -129,17 +129,25 @@ class Constructor: flinger: Flinger, scheme: Scheme, icon_extractor: ShapeExtractor, - check_level=lambda x: True, - mode: str = "normal", - seed: str = "", + options, ) -> None: - self.check_level = check_level - self.mode: str = mode - self.seed: str = seed self.osm_data: OSMData = osm_data self.flinger: Flinger = flinger self.scheme: Scheme = scheme self.icon_extractor = icon_extractor + self.options = options + + if options.level: + if options.level == "overground": + self.check_level = check_level_overground + elif options.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) + ) + else: + self.check_level = lambda x: True self.points: list[Point] = [] self.figures: list[StyledFigure] = [] @@ -195,10 +203,10 @@ class Constructor: return center_point, center_coordinates = line_center(outers[0], self.flinger) - if self.mode in [AUTHOR_MODE, TIME_MODE]: + if self.options.mode in [AUTHOR_MODE, TIME_MODE]: color: Color - if self.mode == AUTHOR_MODE: - color = get_user_color(line.user, self.seed) + if self.options.mode == AUTHOR_MODE: + color = get_user_color(line.user, self.options.seed) else: # self.mode == TIME_MODE color = get_time_color(line.timestamp, self.osm_data.time) self.draw_special_mode(inners, line, outers, color) @@ -207,7 +215,11 @@ class Constructor: if not line.tags: return - if "building:part" in line.tags or "building" in line.tags: + building_mode: BuildingMode = BuildingMode(self.options.buildings) + if "building" in line.tags or ( + building_mode == BuildingMode.ISOMETRIC + and "building:part" in line.tags + ): self.add_building( Building(line.tags, inners, outers, self.flinger, self.scheme) ) @@ -276,9 +288,7 @@ class Constructor: self.points.append(point) def draw_special_mode(self, inners, line, outers, color) -> None: - """ - Add figure for special mode: time or author. - """ + """Add figure for special mode: time or author.""" style: dict[str, Any] = { "fill": "none", "stroke": color.hex, @@ -289,9 +299,7 @@ class Constructor: ) def construct_relations(self) -> None: - """ - Construct Röntgen ways from OSM relations. - """ + """Construct Röntgen ways from OSM relations.""" for relation_id in self.osm_data.relations: relation: OSMRelation = self.osm_data.relations[relation_id] tags = relation.tags @@ -344,13 +352,13 @@ class Constructor: icon_set: IconSet draw_outline: bool = True - if self.mode in [TIME_MODE, AUTHOR_MODE]: + if self.options.mode in [TIME_MODE, AUTHOR_MODE]: if not tags: return color: Color = DEFAULT_COLOR - if self.mode == AUTHOR_MODE: - color = get_user_color(node.user, self.seed) - if self.mode == TIME_MODE: + if self.options.mode == AUTHOR_MODE: + color = get_user_color(node.user, self.options.seed) + if self.options.mode == TIME_MODE: color = get_time_color(node.timestamp, self.osm_data.time) dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID) icon_set = IconSet( @@ -382,3 +390,37 @@ class Constructor: priority=priority, draw_outline=draw_outline ) # fmt: skip self.points.append(point) + + +def check_level_number(tags: dict[str, Any], level: float): + """Check if element described by tags is no the specified level.""" + if "level" in tags: + levels = map(float, tags["level"].replace(",", ".").split(";")) + if level not in levels: + return False + else: + return False + return True + + +def check_level_overground(tags: dict[str, Any]) -> bool: + """Check if element described by tags is overground.""" + if "level" in tags: + try: + levels = map(float, tags["level"].replace(",", ".").split(";")) + for level in levels: + if level <= 0: + return False + except ValueError: + pass + if "layer" in tags: + try: + levels = map(float, tags["layer"].replace(",", ".").split(";")) + for level in levels: + if level <= 0: + return False + except ValueError: + pass + if "parking" in tags and tags["parking"] == "underground": + return False + return True diff --git a/roentgen/direction.py b/roentgen/direction.py index 8117d5f..67d18fb 100644 --- a/roentgen/direction.py +++ b/roentgen/direction.py @@ -17,9 +17,7 @@ DEFAULT_ANGLE: float = np.pi / 30 def degree_to_radian(degree: float) -> float: - """ - Convert value in degrees to radians. - """ + """Convert value in degrees to radians.""" return degree / 180 * np.pi diff --git a/roentgen/element.py b/roentgen/element.py index 05b0aa9..99b1a60 100644 --- a/roentgen/element.py +++ b/roentgen/element.py @@ -1,3 +1,6 @@ +""" +Drawing separate map elements. +""" import logging import sys from pathlib import Path @@ -5,10 +8,10 @@ from pathlib import Path import numpy as np import svgwrite -from roentgen.workspace import workspace from roentgen.icon import ShapeExtractor from roentgen.point import Point from roentgen.scheme import LineStyle, Scheme +from roentgen.workspace import workspace def draw_element(options) -> None: diff --git a/roentgen/figure.py b/roentgen/figure.py index c599263..69930a2 100644 --- a/roentgen/figure.py +++ b/roentgen/figure.py @@ -109,6 +109,13 @@ class Building(Figure): if height: self.min_height = height + def draw(self, svg: Drawing, flinger: Flinger): + """Draw simple building shape.""" + path: Path = Path(d=self.get_path(flinger)) + path.update(self.line_style.style) + path.update({"stroke-linejoin": "round"}) + svg.add(path) + def draw_shade(self, building_shade, flinger: Flinger) -> None: """Draw shade casted by the building.""" scale: float = flinger.get_scale() / 3.0 diff --git a/roentgen/flinger.py b/roentgen/flinger.py index 208335b..bfe5b21 100644 --- a/roentgen/flinger.py +++ b/roentgen/flinger.py @@ -5,7 +5,7 @@ from typing import Optional import numpy as np -from roentgen.util import MinMax +from roentgen.boundary_box import BoundaryBox __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" @@ -45,7 +45,7 @@ class Flinger: def __init__( self, - geo_boundaries: MinMax, + geo_boundaries: BoundaryBox, scale: float = 18, border: np.array = np.array((0, 0)), ) -> None: @@ -53,14 +53,14 @@ class Flinger: :param geo_boundaries: minimum and maximum latitude and longitude :param scale: OSM zoom level """ - self.geo_boundaries: MinMax = geo_boundaries + self.geo_boundaries: BoundaryBox = geo_boundaries self.border = border self.ratio: float = ( osm_zoom_level_to_pixels_per_meter(scale) * EQUATOR_LENGTH / 360 ) self.size: np.array = border * 2 + self.ratio * ( - pseudo_mercator(self.geo_boundaries.max_) - - pseudo_mercator(self.geo_boundaries.min_) + pseudo_mercator(self.geo_boundaries.max_()) + - pseudo_mercator(self.geo_boundaries.min_()) ) self.pixels_per_meter = osm_zoom_level_to_pixels_per_meter(scale) @@ -74,7 +74,7 @@ class Flinger: """ result: np.array = self.border + self.ratio * ( pseudo_mercator(coordinates) - - pseudo_mercator(self.geo_boundaries.min_) + - pseudo_mercator(self.geo_boundaries.min_()) ) # Invert y axis on coordinate plane. diff --git a/roentgen/grid.py b/roentgen/grid.py index 78282f5..8d55018 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -48,9 +48,7 @@ class IconCollection: icons: list[Icon] = [] def add() -> Icon: - """ - Construct icon and add it to the list. - """ + """Construct icon and add it to the list.""" specifications = [ ShapeSpecification.from_structure(x, extractor, scheme) for x in current_set diff --git a/roentgen/icon.py b/roentgen/icon.py index aeae595..7a6246e 100644 --- a/roentgen/icon.py +++ b/roentgen/icon.py @@ -118,9 +118,7 @@ class Shape: def parse_length(text: str) -> float: - """ - Parse length from SVG attribute. - """ + """Parse length from SVG attribute.""" if text.endswith("px"): text = text[:-2] return float(text) @@ -319,9 +317,7 @@ class ShapeSpecification: ) def is_default(self) -> bool: - """ - Check whether shape is default. - """ + """Check whether shape is default.""" return self.shape.id_ == DEFAULT_SHAPE_ID def draw( @@ -389,15 +385,11 @@ class Icon: shape_specifications: list[ShapeSpecification] def get_shape_ids(self) -> list[str]: - """ - Get all shape identifiers in the icon. - """ + """Get all shape identifiers in the icon.""" return [x.shape.id_ for x in self.shape_specifications] def get_names(self) -> list[str]: - """ - Gat all shape names in the icon. - """ + """Get all shape names in the icon.""" return [ (x.shape.name if x.shape.name else "unknown") for x in self.shape_specifications @@ -462,15 +454,11 @@ class Icon: svg.write(output_file) def is_default(self) -> bool: - """ - Check whether first shape is default. - """ + """Check whether first shape is default.""" return self.shape_specifications[0].is_default() def recolor(self, color: Color, white: Optional[Color] = None) -> None: - """ - Paint all shapes in the color. - """ + """Paint all shapes in the color.""" for shape_specification in self.shape_specifications: if shape_specification.color == Color("white") and white: shape_specification.color = white @@ -480,9 +468,7 @@ class Icon: def add_specifications( self, specifications: list[ShapeSpecification] ) -> None: - """ - Add shape specifications to the icon. - """ + """Add shape specifications to the icon.""" self.shape_specifications += specifications def __eq__(self, other) -> bool: diff --git a/roentgen/main.py b/roentgen/main.py index 2064738..80829b6 100644 --- a/roentgen/main.py +++ b/roentgen/main.py @@ -1,23 +1,16 @@ """ Röntgen entry point. - -Author: Sergey Vartanov (me@enzet.ru). """ import argparse import logging import sys from pathlib import Path -from roentgen.element import draw_element -from roentgen.grid import draw_icons -from roentgen.scheme import Scheme from roentgen.ui import parse_options from roentgen.workspace import Workspace - -def init_scheme(workspace: Workspace) -> Scheme: - """Initialize default scheme.""" - return Scheme(workspace.DEFAULT_SCHEME_PATH) +__author__ = "Sergey Vartanov" +__email__ = "me@enzet.ru" def main() -> None: @@ -38,6 +31,8 @@ def main() -> None: tile.ui(arguments) elif arguments.command == "icons": + from roentgen.grid import draw_icons + draw_icons() elif arguments.command == "mapcss": @@ -46,6 +41,8 @@ def main() -> None: mapcss.ui(arguments) elif arguments.command == "element": + from roentgen.element import draw_element + draw_element(arguments) elif arguments.command == "server": @@ -54,9 +51,10 @@ def main() -> None: server.ui(arguments) elif arguments.command == "taginfo": + from roentgen.scheme import Scheme from roentgen.taginfo import write_taginfo_project_file - write_taginfo_project_file(init_scheme(workspace)) + write_taginfo_project_file(Scheme(workspace.DEFAULT_SCHEME_PATH)) if __name__ == "__main__": diff --git a/roentgen/mapcss.py b/roentgen/mapcss.py index 5e410f6..5b8c320 100644 --- a/roentgen/mapcss.py +++ b/roentgen/mapcss.py @@ -140,9 +140,7 @@ class MapCSSWriter: return selector def write(self, output_file: TextIO) -> None: - """ - Construct icon selectors for MapCSS 0.2 scheme. - """ + """Construct icon selectors for MapCSS 0.2 scheme.""" output_file.write(HEADER + "\n\n") if self.add_ways: @@ -178,9 +176,7 @@ class MapCSSWriter: def ui(options) -> None: - """ - Write MapCSS 0.2 scheme. - """ + """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 ff02605..6dee615 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -1,10 +1,10 @@ """ Simple OpenStreetMap renderer. """ +import logging from pathlib import Path from typing import Any, Iterator -import logging import numpy as np import svgwrite from colour import Color @@ -12,17 +12,17 @@ from svgwrite.container import Group from svgwrite.path import Path as SVGPath from svgwrite.shapes import Rect -from roentgen.icon import ShapeExtractor -from roentgen.osm_getter import NetworkError, get_osm +from roentgen.boundary_box import BoundaryBox from roentgen.constructor import Constructor from roentgen.figure import Road from roentgen.flinger import Flinger +from roentgen.icon import ShapeExtractor +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, BoundaryBox, TIME_MODE, progress_bar -from roentgen.util import MinMax +from roentgen.ui import AUTHOR_MODE, BuildingMode, TIME_MODE, progress_bar from roentgen.workspace import workspace __author__ = "Sergey Vartanov" @@ -35,24 +35,15 @@ class Map: """ def __init__( - self, - flinger: Flinger, - svg: svgwrite.Drawing, - scheme: Scheme, - overlap: int = 12, - mode: str = "normal", - label_mode: str = "main", + self, flinger: Flinger, svg: svgwrite.Drawing, scheme: Scheme, options ) -> None: - self.overlap: int = overlap - self.mode: str = mode - self.label_mode: str = label_mode - self.flinger: Flinger = flinger self.svg: svgwrite.Drawing = svg self.scheme: Scheme = scheme + self.options = options self.background_color: Color = self.scheme.get_color("background_color") - if self.mode in [AUTHOR_MODE, TIME_MODE]: + if self.options.mode in [AUTHOR_MODE, TIME_MODE]: self.background_color: Color = Color("#111111") def draw(self, constructor: Constructor) -> None: @@ -89,11 +80,11 @@ class Map: # All other points - if self.overlap == 0: + if self.options.overlap == 0: occupied = None else: occupied = Occupied( - self.flinger.size[0], self.flinger.size[1], self.overlap + self.flinger.size[0], self.flinger.size[1], self.options.overlap ) nodes = sorted(constructor.points, key=lambda x: -x.priority) @@ -114,17 +105,24 @@ class Map: steps * 2 + index, steps * 3, step=10, text="Drawing texts" ) if ( - self.mode not in [TIME_MODE, AUTHOR_MODE] - and self.label_mode != "no" + self.options.mode not in [TIME_MODE, AUTHOR_MODE] + and self.options.label_mode != "no" ): - point.draw_texts(self.svg, occupied, self.label_mode) + point.draw_texts(self.svg, occupied, self.options.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_shade: Group = Group(opacity=0.1) + building_mode: BuildingMode = BuildingMode(self.options.buildings) + + if building_mode == BuildingMode.FLAT: + for building in constructor.buildings: + building.draw(self.svg, self.flinger) + return + scale: float = self.flinger.get_scale() / 3.0 + building_shade: Group = Group(opacity=0.1) for building in constructor.buildings: building.draw_shade(building_shade, self.flinger) self.svg.add(building_shade) @@ -150,9 +148,7 @@ class Map: def draw_road( self, road: Road, color: Color, extra_width: float = 0 ) -> None: - """ - Draw road as simple SVG path. - """ + """Draw road as simple SVG path.""" self.flinger.get_scale() if road.width is not None: width = road.width @@ -172,9 +168,7 @@ class Map: self.svg.add(path) def draw_roads(self, roads: Iterator[Road]) -> None: - """ - Draw road as simple SVG path. - """ + """Draw road as simple SVG path.""" nodes: dict[OSMNode, set[RoadPart]] = {} for road in roads: @@ -203,44 +197,6 @@ class Map: intersection.draw(self.svg, True) -def check_level_number(tags: dict[str, Any], level: float): - """ - Check if element described by tags is no the specified level. - """ - if "level" in tags: - levels = map(float, tags["level"].replace(",", ".").split(";")) - if level not in levels: - return False - else: - return False - return True - - -def check_level_overground(tags: dict[str, Any]) -> bool: - """ - Check if element described by tags is overground. - """ - if "level" in tags: - try: - levels = map(float, tags["level"].replace(",", ".").split(";")) - for level in levels: - if level <= 0: - return False - except ValueError: - pass - if "layer" in tags: - try: - levels = map(float, tags["layer"].replace(",", ".").split(";")) - for level in levels: - if level <= 0: - return False - except ValueError: - pass - if "parking" in tags and tags["parking"] == "underground": - return False - return True - - def ui(options) -> None: """ Röntgen entry point. @@ -276,27 +232,21 @@ def ui(options) -> None: min_: np.array max_: np.array osm_data: OSMData + view_box: BoundaryBox if input_file_names[0].name.endswith(".json"): reader: OverpassReader = OverpassReader() reader.parse_json_file(input_file_names[0]) osm_data = reader.osm_data - view_box = MinMax( - np.array( - (osm_data.boundary_box[0].min_, osm_data.boundary_box[1].min_) - ), - np.array( - (osm_data.boundary_box[0].max_, osm_data.boundary_box[1].max_) - ), - ) + view_box = boundary_box else: is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE] osm_reader = OSMReader(is_full=is_full) for file_name in input_file_names: if not file_name.is_file(): - print(f"Fatal: no such file: {file_name}.") + logging.fatal(f"No such file: {file_name}.") exit(1) osm_reader.parse_osm_file(file_name) @@ -304,10 +254,7 @@ def ui(options) -> None: osm_data = osm_reader.osm_data if options.boundary_box: - view_box = MinMax( - np.array((boundary_box.bottom, boundary_box.left)), - np.array((boundary_box.top, boundary_box.right)), - ) + view_box = boundary_box else: view_box = osm_data.view_box @@ -321,48 +268,18 @@ def ui(options) -> None: workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH ) - if options.level: - if options.level == "overground": - check_level = check_level_overground - elif options.level == "underground": - - def check_level(x) -> bool: - """Draw underground objects.""" - return not check_level_overground(x) - - else: - - def check_level(x) -> bool: - """Draw objects on the specified level.""" - return not check_level_number(x, float(options.level)) - - else: - - def check_level(_) -> bool: - """Draw objects on any level.""" - return True - constructor: Constructor = Constructor( - osm_data, - flinger, - scheme, - icon_extractor, - check_level, - options.mode, - options.seed, + osm_data=osm_data, + flinger=flinger, + scheme=scheme, + icon_extractor=icon_extractor, + options=options, ) constructor.construct() - painter: Map = Map( - overlap=options.overlap, - mode=options.mode, - label_mode=options.label_mode, - flinger=flinger, - svg=svg, - scheme=scheme, - ) + painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme, options=options) painter.draw(constructor) - print(f"Writing output SVG to {options.output_file_name}...") + logging.info(f"Writing output SVG to {options.output_file_name}...") with open(options.output_file_name, "w") as output_file: svg.write(output_file) diff --git a/roentgen/osm_getter.py b/roentgen/osm_getter.py index 955839d..3d7d701 100644 --- a/roentgen/osm_getter.py +++ b/roentgen/osm_getter.py @@ -9,7 +9,7 @@ from pathlib import Path import urllib3 -from roentgen.ui import BoundaryBox +from roentgen.boundary_box import BoundaryBox __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py index 34354a0..fbd034b 100644 --- a/roentgen/osm_reader.py +++ b/roentgen/osm_reader.py @@ -11,6 +11,7 @@ from xml.etree import ElementTree import numpy as np +from roentgen.boundary_box import BoundaryBox from roentgen.util import MinMax __author__ = "Sergey Vartanov" @@ -37,9 +38,7 @@ STAGES_OF_DECAY: list[str] = [ def parse_float(string: str) -> Optional[float]: - """ - Parse string representation of a float or integer value. - """ + """Parse string representation of a float or integer value.""" try: return float(string) except (TypeError, ValueError): @@ -108,7 +107,7 @@ class OSMNode(Tagged): super().__init__() self.id_: Optional[int] = None - self.coordinates: Optional[np.array] = None + self.coordinates: Optional[np.ndarray] = None self.visible: Optional[str] = None self.changeset: Optional[str] = None @@ -118,10 +117,8 @@ class OSMNode(Tagged): @classmethod def from_xml_structure(cls, element, is_full: bool = False) -> "OSMNode": - """ - Parse node from OSM XML `` element. - """ - node = cls() + """Parse node from OSM XML `` element.""" + node: "OSMNode" = cls() attributes = element.attrib node.id_ = int(attributes["id"]) node.coordinates = np.array( @@ -178,9 +175,7 @@ class OSMWay(Tagged): @classmethod def from_xml_structure(cls, element, nodes, is_full: bool) -> "OSMWay": - """ - Parse way from OSM XML `` element. - """ + """Parse way from OSM XML `` element.""" way = cls(int(element.attrib["id"])) if is_full: way.visible = element.attrib["visible"] @@ -215,15 +210,11 @@ class OSMWay(Tagged): return self def is_cycle(self) -> bool: - """ - Is way a cycle way or an area boundary. - """ + """Is way a cycle way or an area boundary.""" return self.nodes[0] == self.nodes[-1] def try_to_glue(self, other: "OSMWay") -> Optional["OSMWay"]: - """ - Create new combined way if ways share endpoints. - """ + """Create new combined way if ways share endpoints.""" if self.nodes[0] == other.nodes[0]: return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes) if self.nodes[0] == other.nodes[-1]: @@ -255,9 +246,7 @@ class OSMRelation(Tagged): @classmethod def from_xml_structure(cls, element, is_full: bool) -> "OSMRelation": - """ - Parse relation from OSM XML `` element. - """ + """Parse relation from OSM XML `` element.""" attributes = element.attrib relation = cls(int(attributes["id"])) if is_full: @@ -321,33 +310,24 @@ class OSMData: self.authors: set[str] = set() self.time: MinMax = MinMax() - self.boundary_box: list[MinMax] = [MinMax(), MinMax()] - self.view_box = None + self.view_box: Optional[BoundaryBox] = None def add_node(self, node: OSMNode) -> None: - """ - Add node and update map parameters. - """ + """Add node and update map parameters.""" self.nodes[node.id_] = node if node.user: self.authors.add(node.user) self.time.update(node.timestamp) - self.boundary_box[0].update(node.coordinates[0]) - self.boundary_box[1].update(node.coordinates[1]) def add_way(self, way: OSMWay) -> None: - """ - Add way and update map parameters. - """ + """Add way and update map parameters.""" self.ways[way.id_] = way if way.user: self.authors.add(way.user) self.time.update(way.timestamp) def add_relation(self, relation: OSMRelation) -> None: - """ - Add relation and update map parameters. - """ + """Add relation and update map parameters.""" self.relations[relation.id_] = relation @@ -362,9 +342,7 @@ class OverpassReader: self.osm_data = OSMData() def parse_json_file(self, file_name: Path) -> OSMData: - """ - Parse JSON structure from the file and construct map. - """ + """Parse JSON structure from the file and construct map.""" with file_name.open() as input_file: structure = json.load(input_file) @@ -460,15 +438,11 @@ class OSMReader: return self.osm_data def parse_bounds(self, element) -> None: - """ - Parse view box from XML element. - """ + """Parse view box from XML element.""" attributes = element.attrib - self.osm_data.view_box = MinMax( - np.array( - (float(attributes["minlat"]), float(attributes["minlon"])) - ), - np.array( - (float(attributes["maxlat"]), float(attributes["maxlon"])) - ), + self.osm_data.view_box = BoundaryBox( + float(attributes["minlon"]), + float(attributes["minlat"]), + float(attributes["maxlon"]), + float(attributes["maxlat"]), ) diff --git a/roentgen/point.py b/roentgen/point.py index ab8402f..83983cb 100644 --- a/roentgen/point.py +++ b/roentgen/point.py @@ -30,17 +30,13 @@ class Occupied: self.overlap: float = overlap def check(self, point: np.array) -> bool: - """ - Check whether point is already occupied by other elements. - """ + """Check whether point is already occupied by other elements.""" if 0 <= point[0] < self.width and 0 <= point[1] < self.height: return self.matrix[point[0], point[1]] return True def register(self, point) -> None: - """ - Register that point is occupied by an element. - """ + """Register that point is occupied by an element.""" if 0 <= point[0] < self.width and 0 <= point[1] < self.height: self.matrix[point[0], point[1]] = True assert self.matrix[point[0], point[1]] @@ -86,9 +82,7 @@ class Point(Tagged): def draw_main_shapes( self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None ) -> None: - """ - Draw main shape for one node. - """ + """Draw main shape for one node.""" keys_left = [x for x in self.tags.keys() if x not in self.processed] if ( self.icon_set.main_icon.is_default() @@ -107,9 +101,7 @@ class Point(Tagged): def draw_extra_shapes( self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None ) -> None: - """ - Draw secondary shapes. - """ + """Draw secondary shapes.""" if not self.icon_set.extra_icons or not self.main_icon_painted: return @@ -141,9 +133,7 @@ class Point(Tagged): occupied, tags: Optional[dict[str, str]] = None, ) -> bool: - """ - Draw one combined icon and its outline. - """ + """Draw one combined icon and its outline.""" # Down-cast floats to integers to make icons pixel-perfect. position = list(map(int, position)) @@ -173,9 +163,7 @@ class Point(Tagged): occupied: Optional[Occupied] = None, label_mode: str = "main", ) -> None: - """ - Draw all labels. - """ + """Draw all labels.""" labels: list[Label] if label_mode == "main": diff --git a/roentgen/road.py b/roentgen/road.py index 2afa735..6bcef58 100644 --- a/roentgen/road.py +++ b/roentgen/road.py @@ -81,14 +81,13 @@ class RoadPart: self.left_outer = None self.right_outer = None self.point_a = None + self.point_middle = None @classmethod def from_nodes( cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, road, scale ) -> "RoadPart": - """ - Construct road part from OSM nodes. - """ + """Construct road part from OSM nodes.""" lanes = [Lane(road.width / road.lanes)] * road.lanes return cls( @@ -99,9 +98,7 @@ class RoadPart: ) def update(self) -> None: - """ - Compute additional points. - """ + """Compute additional points.""" if self.left_connection is not None: self.right_projection = ( self.left_connection + self.right_vector - self.left_vector @@ -136,15 +133,11 @@ class RoadPart: self.point_a = self.point_middle def get_angle(self) -> float: - """ - Get an angle between line and x axis. - """ + """Get an angle between line and x axis.""" return compute_angle(self.point_2 - self.point_1) def draw_normal(self, drawing: svgwrite.Drawing): - """ - Draw some debug lines. - """ + """Draw some debug lines.""" line = drawing.path( ("M", self.point_1, "L", self.point_2), fill="none", @@ -154,9 +147,7 @@ class RoadPart: drawing.add(line) def draw_debug(self, drawing: svgwrite.Drawing): - """ - Draw some debug lines. - """ + """Draw some debug lines.""" line = drawing.path( ("M", self.point_1, "L", self.point_2), fill="none", @@ -237,9 +228,7 @@ class RoadPart: # self.draw_entrance(drawing, True) def draw(self, drawing: svgwrite.Drawing): - """ - Draw road part. - """ + """Draw road part.""" if self.left_connection is not None: path_commands = [ "M", self.point_2 + self.right_vector, @@ -251,9 +240,7 @@ class RoadPart: drawing.add(drawing.path(path_commands, fill="#CCCCCC")) def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool = False): - """ - Draw intersection entrance part. - """ + """Draw intersection entrance part.""" if ( self.left_connection is not None and self.right_connection is not None @@ -277,9 +264,7 @@ class RoadPart: drawing.add(drawing.path(path_commands, fill="#88FF88")) def draw_lanes(self, drawing: svgwrite.Drawing, scale: float): - """ - Draw lane delimiters. - """ + """Draw lane delimiters.""" for lane in self.lanes: shift = self.right_vector - self.turned * lane.get_width(scale) path = drawing.path( @@ -340,9 +325,7 @@ class Intersection: part_2.update() def draw(self, drawing: svgwrite.Drawing, is_debug: bool = False) -> None: - """ - Draw all road parts and intersection. - """ + """Draw all road parts and intersection.""" inner_commands = ["M"] for part in self.parts: inner_commands += [part.left_connection, "L"] diff --git a/roentgen/scheme.py b/roentgen/scheme.py index b803cef..d564779 100644 --- a/roentgen/scheme.py +++ b/roentgen/scheme.py @@ -413,12 +413,6 @@ class Scheme: if main_icon and color: main_icon.recolor(color) - # keys_left = [ - # x - # for x in tags.keys() - # if x not in processed and not self.is_no_drawable(x) - # ] - default_shape = extractor.get_shape(DEFAULT_SHAPE_ID) if not main_icon: main_icon = Icon([ShapeSpecification(default_shape)]) diff --git a/roentgen/server.py b/roentgen/server.py index a421f32..182198b 100644 --- a/roentgen/server.py +++ b/roentgen/server.py @@ -1,5 +1,5 @@ """ -Röntgen tile server for sloppy maps. +Röntgen tile server for slippy maps. """ import logging from http.server import HTTPServer, SimpleHTTPRequestHandler @@ -15,13 +15,14 @@ __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -class Handler(SimpleHTTPRequestHandler): +class _Handler(SimpleHTTPRequestHandler): """ HTTP request handler that process sloppy map tile requests. """ cache: Path = Path("cache") update_cache: bool = False + options = None def __init__( self, request: bytes, client_address: tuple[str, int], server @@ -33,22 +34,25 @@ class Handler(SimpleHTTPRequestHandler): parts: list[str] = self.path.split("/") if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"): return - zoom: int = int(parts[2]) + + scale: int = int(parts[2]) x: int = int(parts[3]) y: int = int(parts[4]) tile_path: Path = workspace.get_tile_path() - png_path = tile_path / f"tile_{zoom}_{x}_{y}.png" + png_path: Path = tile_path / f"tile_{scale}_{x}_{y}.png" + if self.update_cache: - svg_path = tile_path / f"tile_{zoom}_{x}_{y}.svg" + svg_path: Path = png_path.with_suffix(".svg") if not png_path.exists(): if not svg_path.exists(): - tile = Tile(x, y, zoom) - tile.draw(tile_path, self.cache) + tile = Tile(x, y, scale) + tile.draw(tile_path, self.cache, self.options) with svg_path.open() as input_file: cairosvg.svg2png( file_obj=input_file, write_to=str(png_path) ) logging.info(f"SVG file is rasterized to {png_path}.") + if png_path.exists(): with png_path.open("rb") as input_file: self.send_response(200) @@ -63,11 +67,12 @@ def ui(options) -> None: server: Optional[HTTPServer] = None try: port: int = 8080 - handler = Handler + handler = _Handler handler.cache = Path(options.cache) + handler.options = options server: HTTPServer = HTTPServer(("", port), handler) - server.serve_forever() logging.info(f"Server started on port {port}.") + server.serve_forever() finally: if server: server.socket.close() diff --git a/roentgen/text.py b/roentgen/text.py index d62ac7e..6a55ff3 100644 --- a/roentgen/text.py +++ b/roentgen/text.py @@ -75,16 +75,12 @@ def format_voltage(value: str) -> str: def format_frequency(value: str) -> str: - """ - Format frequency value to more human-readable form. - """ + """Format frequency value to more human-readable form.""" return f"{value} " def get_text(tags: dict[str, Any], processed: set[str]) -> list[Label]: - """ - Get text representation of writable tags. - """ + """Get text representation of writable tags.""" texts: list[Label] = [] values: list[str] = [] diff --git a/roentgen/tile.py b/roentgen/tile.py index 9a719a4..0460232 100644 --- a/roentgen/tile.py +++ b/roentgen/tile.py @@ -21,8 +21,7 @@ from roentgen.mapper import Map from roentgen.osm_getter import NetworkError, get_osm from roentgen.osm_reader import OSMData, OSMReader from roentgen.scheme import Scheme -from roentgen.ui import BoundaryBox -from roentgen.util import MinMax +from roentgen.boundary_box import BoundaryBox from roentgen.workspace import workspace __author__ = "Sergey Vartanov" @@ -116,36 +115,33 @@ class Tile: f"https://tile.openstreetmap.org/{self.scale}/{self.x}/{self.y}.png" ) - def draw(self, directory_name: Path, cache_path: Path) -> None: + def draw(self, directory_name: Path, cache_path: Path, options) -> 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 """ 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) + self.draw_with_osm_data(osm_data, directory_name, options) def draw_with_osm_data( - self, osm_data: OSMData, directory_name: Path + self, osm_data: OSMData, directory_name: Path, options ) -> None: """Draw SVG and PNG tile using OpenStreetMap data.""" - latitude_1, longitude_1 = self.get_coordinates() - latitude_2, longitude_2 = Tile( + top, left = self.get_coordinates() + bottom, right = Tile( self.x + 1, self.y + 1, self.scale ).get_coordinates() - min_: np.array = np.array( - (min(latitude_1, latitude_2), min(longitude_1, longitude_2)) + flinger: Flinger = Flinger( + BoundaryBox(left, bottom, right, top), self.scale ) - max_: np.array = np.array( - (max(latitude_1, latitude_2), max(longitude_1, longitude_2)) - ) - flinger: Flinger = Flinger(MinMax(min_, max_), self.scale) size: np.array = flinger.size output_file_name: Path = self.get_file_name(directory_name) @@ -158,11 +154,13 @@ class Tile: ) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) constructor: Constructor = Constructor( - osm_data, flinger, scheme, icon_extractor + osm_data, flinger, scheme, icon_extractor, options ) constructor.construct() - painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme) + painter: Map = Map( + flinger=flinger, svg=svg, scheme=scheme, options=options + ) painter.draw(constructor) with output_file_name.open("w") as output_file: @@ -217,13 +215,16 @@ class Tiles: return cls(tiles, tile_1, tile_2, scale, extended_boundary_box) - def draw_separately(self, directory: Path, cache_path: Path) -> None: + def draw_separately( + self, directory: Path, cache_path: Path, options + ) -> None: """ Draw set of tiles as SVG file separately and rasterize them into a set of PNG files with cairosvg. :param directory: directory for tiles :param cache_path: directory for temporary OSM files + :param options: drawing configuration """ cache_file_path: Path = ( cache_path / f"{self.boundary_box.get_format()}.osm" @@ -234,7 +235,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) + tile.draw_with_osm_data(osm_data, directory, options) else: logging.debug(f"File {file_path} already exists.") @@ -252,18 +253,19 @@ 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) -> None: + def draw(self, directory: Path, cache_path: Path, options) -> 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 """ if self.tiles_exist(directory): return - self.draw_image(cache_path) + self.draw_image(cache_path, options) input_path: Path = self.get_file_path(cache_path).with_suffix(".png") with input_path.open("rb") as input_file: @@ -288,11 +290,12 @@ 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) -> None: + def draw_image(self, cache_path: Path, options) -> None: """ Draw all tiles as one picture. :param cache_path: directory for temporary SVG file and OSM files + :param options: drawing configuration """ output_path: Path = self.get_file_path(cache_path) @@ -303,27 +306,29 @@ class Tiles: get_osm(self.boundary_box, cache_file_path) osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path) - latitude_2, longitude_1 = self.tile_1.get_coordinates() - latitude_1, longitude_2 = Tile( + top, left = self.tile_1.get_coordinates() + bottom, right = Tile( self.tile_2.x + 1, self.tile_2.y + 1, self.scale ).get_coordinates() - min_: np.ndarray = np.array((latitude_1, longitude_1)) - max_: np.ndarray = np.array((latitude_2, longitude_2)) - flinger: Flinger = Flinger(MinMax(min_, max_), self.scale) + flinger: Flinger = Flinger( + BoundaryBox(left, bottom, right, top), self.scale + ) extractor: ShapeExtractor = ShapeExtractor( workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH ) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) constructor: Constructor = Constructor( - osm_data, flinger, scheme, extractor + osm_data, flinger, scheme, extractor, options=options ) constructor.construct() svg: svgwrite.Drawing = svgwrite.Drawing( str(output_path), size=flinger.size ) - map_: Map = Map(flinger=flinger, svg=svg, scheme=scheme) + map_: Map = Map( + flinger=flinger, svg=svg, scheme=scheme, options=options + ) map_.draw(constructor) logging.info(f"Writing output SVG {output_path}...") @@ -351,13 +356,13 @@ def ui(options) -> None: ) tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale) try: - tile.draw(directory, Path(options.cache)) + tile.draw(directory, Path(options.cache), options) 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)) + tile.draw(directory, Path(options.cache), options) elif options.boundary_box: boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text( options.boundary_box @@ -365,7 +370,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)) + tiles.draw(directory, Path(options.cache), options) else: logging.fatal( "Specify either --coordinates, --boundary-box, or --tile." diff --git a/roentgen/ui.py b/roentgen/ui.py index 519678f..7011fe2 100644 --- a/roentgen/ui.py +++ b/roentgen/ui.py @@ -2,16 +2,12 @@ Command-line user interface. """ import argparse -import re import sys __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" -import logging -from dataclasses import dataclass - -import numpy as np +from enum import Enum from roentgen.osm_reader import STAGES_OF_DECAY @@ -21,8 +17,15 @@ BOXES_LENGTH: int = len(BOXES) AUTHOR_MODE: str = "author" TIME_MODE: str = "time" -LATITUDE_MAX_DIFFERENCE: float = 0.5 -LONGITUDE_MAX_DIFFERENCE: float = 0.5 + +class BuildingMode(Enum): + """ + Building drawing mode. + """ + + FLAT = "flat" + ISOMETRIC = "isometric" + ISOMETRIC_NO_PARTS = "isometric-no-parts" def parse_options(args) -> argparse.Namespace: @@ -32,8 +35,14 @@ def parse_options(args) -> argparse.Namespace: ) subparser = parser.add_subparsers(dest="command") - add_render_arguments(subparser.add_parser("render")) - add_tile_arguments(subparser.add_parser("tile")) + tile_parser = subparser.add_parser("tile") + add_tile_arguments(tile_parser) + add_map_arguments(tile_parser) + + render_parser = subparser.add_parser("render") + add_render_arguments(render_parser) + add_map_arguments(render_parser) + add_server_arguments(subparser.add_parser("server")) add_element_arguments(subparser.add_parser("element")) add_mapcss_arguments(subparser.add_parser("mapcss")) @@ -46,13 +55,36 @@ def parse_options(args) -> argparse.Namespace: return arguments -def add_tile_arguments(parser: argparse.ArgumentParser) -> None: - """Add arguments for tile command.""" +def add_map_arguments(parser: argparse.ArgumentParser) -> None: + """Add map-specific arguments.""" parser.add_argument( - "-c", - "--coordinates", - metavar=",", - help="coordinates of any location inside the tile", + "--buildings", + metavar="", + default="flat", + choices=(x.value for x in BuildingMode), + help="building drawing mode: " + + ", ".join(x.value for x in BuildingMode), + ) + parser.add_argument( + "--mode", + default="normal", + help="map drawing mode", + metavar="", + ) + parser.add_argument( + "--overlap", + dest="overlap", + default=12, + type=int, + help="how many pixels should be left around icons and text", + metavar="", + ) + parser.add_argument( + "--labels", + help="label drawing mode: `no`, `main`, or `all`", + dest="label_mode", + default="main", + metavar="", ) parser.add_argument( "-s", @@ -62,6 +94,21 @@ def add_tile_arguments(parser: argparse.ArgumentParser) -> None: help="OSM zoom level", default=18, ) + parser.add_argument( + "--level", + default="overground", + help="display only this floor level", + ) + + +def add_tile_arguments(parser: argparse.ArgumentParser) -> None: + """Add arguments for tile command.""" + parser.add_argument( + "-c", + "--coordinates", + metavar=",", + help="coordinates of any location inside the tile", + ) parser.add_argument( "-t", "--tile", @@ -126,47 +173,17 @@ def add_render_arguments(parser: argparse.ArgumentParser) -> None: help="geo boundary box; if first value is negative, enclose the value " "with quotes and use space before `-`", ) - parser.add_argument( - "-s", - "--scale", - metavar="", - help="OSM zoom level (may not be integer)", - default=18, - type=float, - ) parser.add_argument( "--cache", help="path for temporary OSM files", default="cache", metavar="", ) - parser.add_argument( - "--labels", - help="label drawing mode: `no`, `main`, or `all`", - dest="label_mode", - default="main", - ) - parser.add_argument( - "--overlap", - dest="overlap", - default=12, - type=int, - help="how many pixels should be left around icons and text", - ) - parser.add_argument( - "--mode", - default="normal", - help="map drawing mode", - ) parser.add_argument( "--seed", default="", help="seed for random", - ) - parser.add_argument( - "--level", - default=None, - help="display only this floor level", + metavar="", ) @@ -219,91 +236,3 @@ def progress_bar( f"{int(length - fill_length - 1) * ' '}▏{text}" ) sys.stdout.write("\033[F") - - -@dataclass -class BoundaryBox: - """ - Rectangle that limit space on the map. - """ - - left: float - bottom: float - right: float - top: float - - @classmethod - def from_text(cls, boundary_box: str): - """ - Parse boundary box string representation. - - Note, that: - left < right - bottom < top - - :param boundary_box: boundary box string representation in the form of - ,,, or simply - ,,,. - """ - boundary_box = boundary_box.replace(" ", "") - - matcher = re.match( - "(?P[0-9.-]*),(?P[0-9.-]*)," - + "(?P[0-9.-]*),(?P[0-9.-]*)", - boundary_box, - ) - - if not matcher: - logging.fatal("Invalid boundary box.") - return None - - try: - left: float = float(matcher.group("left")) - bottom: float = float(matcher.group("bottom")) - right: float = float(matcher.group("right")) - top: float = float(matcher.group("top")) - except ValueError: - logging.fatal("Invalid boundary box.") - return None - - if left >= right: - logging.fatal("Negative horizontal boundary.") - return None - if bottom >= top: - logging.error("Negative vertical boundary.") - return None - if ( - right - left > LONGITUDE_MAX_DIFFERENCE - or top - bottom > LATITUDE_MAX_DIFFERENCE - ): - logging.error("Boundary box is too big.") - return None - - return cls(left, bottom, right, top) - - def get_left_top(self) -> (np.array, np.array): - """Get left top corner of the boundary box.""" - return self.top, self.left - - def get_right_bottom(self) -> (np.array, np.array): - """Get right bottom corner of the boundary box.""" - return self.bottom, self.right - - def round(self) -> "BoundaryBox": - """Round boundary box.""" - self.left = round(self.left * 1000) / 1000 - 0.001 - self.bottom = round(self.bottom * 1000) / 1000 - 0.001 - self.right = round(self.right * 1000) / 1000 + 0.001 - self.top = round(self.top * 1000) / 1000 + 0.001 - - return self - - def get_format(self) -> str: - """ - Get text representation of the boundary box: - ,,,. Coordinates are - rounded to three digits after comma. - """ - return ( - f"{self.left:.3f},{self.bottom:.3f},{self.right:.3f},{self.top:.3f}" - ) diff --git a/roentgen/vector.py b/roentgen/vector.py index d7e5926..08a3db8 100644 --- a/roentgen/vector.py +++ b/roentgen/vector.py @@ -24,9 +24,7 @@ def compute_angle(vector: np.array): def turn_by_angle(vector: np.array, angle: float): - """ - Turn vector by an angle. - """ + """Turn vector by an angle.""" return np.array( ( vector[0] * np.cos(angle) - vector[1] * np.sin(angle), @@ -36,16 +34,12 @@ def turn_by_angle(vector: np.array, angle: float): def norm(vector: np.array) -> np.array: - """ - Compute vector with the same direction and length 1. - """ + """Compute vector with the same direction and length 1.""" return vector / np.linalg.norm(vector) class Line: - """ - Infinity line: Ax + By + C = 0. - """ + """Infinity line: Ax + By + C = 0.""" def __init__(self, start: np.array, end: np.array) -> None: # if start.near(end): @@ -63,15 +57,11 @@ class Line: self.c -= self.a * shift.x + self.b * shift.y def is_parallel(self, other: "Line") -> bool: - """ - If lines are parallel or equal. - """ + """If lines are parallel or equal.""" return np.allclose(other.a * self.b - self.a * other.b, 0) def get_intersection_point(self, other: "Line") -> np.array: - """ - Get point of intersection current line with other. - """ + """Get point of intersection current line with other.""" if other.a * self.b - self.a * other.b == 0: return np.array((0, 0)) diff --git a/scheme/default.yml b/scheme/default.yml index 156ae1f..198aaef 100644 --- a/scheme/default.yml +++ b/scheme/default.yml @@ -689,25 +689,79 @@ node_icons: - tags: {man_made: mast, tower:construction: lattice_guyed} shapes: [lattice_guyed] - tags: {man_made: mast, tower:type: lighting} - shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] + shapes: + - tube + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} - tags: {man_made: mast, tower:type: communication} - shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: lighting, tower:construction: guyed_tube} - shapes: [tube_guyed, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: communication, tower:construction: guyed_tube} - shapes: [tube_guyed, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: lighting, tower:construction: freestanding} - shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: communication, tower:construction: freestanding} - shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: lighting, tower:construction: guyed_lattice} - shapes: [lattice_guyed, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: communication, tower:construction: guyed_lattice} - shapes: [lattice_guyed, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: lighting, tower:construction: lattice} - shapes: [lattice, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: mast, tower:type: communication, tower:construction: lattice} - shapes: [lattice, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] + shapes: + - tube + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: lighting + tower:construction: guyed_tube + shapes: + - tube_guyed + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: communication + tower:construction: guyed_tube + shapes: + - tube_guyed + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: lighting + tower:construction: freestanding + shapes: + - tube + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: communication + tower:construction: freestanding + shapes: + - tube + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: lighting + tower:construction: guyed_lattice + shapes: + - lattice_guyed + - {shape: light_left, offset: [-4, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: communication + tower:construction: guyed_lattice + shapes: + - lattice_guyed + - {shape: wave_left, offset: [-4, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: lighting + tower:construction: lattice + shapes: + - lattice + - {shape: light_left, offset: [-4, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: mast + tower:type: communication + tower:construction: lattice + shapes: + - lattice + - {shape: wave_left, offset: [-4, -3]} + - {shape: wave_right, offset: [3, -3]} - tags: {man_made: tower, tower:construction: guyed_tube} shapes: [tube_guyed] @@ -718,25 +772,79 @@ node_icons: - tags: {man_made: tower, tower:construction: lattice_guyed} shapes: [lattice_guyed] - tags: {man_made: tower, tower:type: lighting} - shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] + shapes: + - tube + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} - tags: {man_made: tower, tower:type: communication} - shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: lighting, tower:construction: guyed_tube} - shapes: [tube_guyed, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: communication, tower:construction: guyed_tube} - shapes: [tube_guyed, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: lighting, tower:construction: freestanding} - shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: communication, tower:construction: freestanding} - shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: lighting, tower:construction: guyed_lattice} - shapes: [lattice_guyed, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: communication, tower:construction: guyed_lattice} - shapes: [lattice_guyed, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: lighting, tower:construction: lattice} - shapes: [lattice, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - - tags: {man_made: tower, tower:type: communication, tower:construction: lattice} - shapes: [lattice, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] + shapes: + - tube + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: lighting + tower:construction: guyed_tube + shapes: + - tube_guyed + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: communication + tower:construction: guyed_tube + shapes: + - tube_guyed + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: lighting + tower:construction: freestanding + shapes: + - tube + - {shape: light_left, offset: [-3, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: communication + tower:construction: freestanding + shapes: + - tube + - {shape: wave_left, offset: [-3, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: lighting + tower:construction: guyed_lattice + shapes: + - lattice_guyed + - {shape: light_left, offset: [-4, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: communication + tower:construction: guyed_lattice + shapes: + - lattice_guyed + - {shape: wave_left, offset: [-4, -3]} + - {shape: wave_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: lighting + tower:construction: lattice + shapes: + - lattice + - {shape: light_left, offset: [-4, -3]} + - {shape: light_right, offset: [3, -3]} + - tags: + man_made: tower + tower:type: communication + tower:construction: lattice + shapes: + - lattice + - {shape: wave_left, offset: [-4, -3]} + - {shape: wave_right, offset: [3, -3]} - tags: {communication:mobile_phone: "yes"} add_shapes: [phone] @@ -873,7 +981,9 @@ node_icons: - group: "Entrances" tags: - tags: {amenity: parking_entrance} - shapes: [{shape: p, offset: [-1, 0]}, {shape: arrow_right, offset: [4, 5]}] + shapes: + - {shape: p, offset: [-1, 0]} + - {shape: arrow_right, offset: [4, 5]} - tags: {amenity: parking_entrance, parking: underground} shapes: [{shape: p, offset: [-1, 0]}, {shape: arrow_down, offset: [4, 5]}] - tags: {amenity: parking_entrance, parking: multi-storey} diff --git a/test/test_boundary_box.py b/test/test_boundary_box.py index 8f229e3..3adfd6c 100644 --- a/test/test_boundary_box.py +++ b/test/test_boundary_box.py @@ -1,13 +1,14 @@ """ Test boundary box. """ -from roentgen.ui import BoundaryBox +from roentgen.boundary_box import BoundaryBox __author__ = "Sergey Vartanov" __email__ = "me@enzet.ru" def test_round_zero_coordinates() -> None: + """Test rounding for zero coordinates.""" assert ( BoundaryBox(0, 0, 0, 0).round().get_format() == "-0.001,-0.001,0.001,0.001" @@ -15,6 +16,7 @@ def test_round_zero_coordinates() -> None: def test_round_coordinates() -> None: + """Test rounding coordinates.""" box: BoundaryBox = BoundaryBox( 10.067596435546875, 46.094186149226466,