Add options to tile generation.

This commit is contained in:
Sergey Vartanov 2021-08-25 23:56:49 +03:00
parent 163fe4060b
commit fe2714c927
26 changed files with 448 additions and 524 deletions

View file

@ -18,7 +18,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install pytest flake8 black pip install black flake8 pytest
pip install -r requirements.txt pip install -r requirements.txt
pip install . pip install .
- name: Test with pytest - name: Test with pytest

View file

@ -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). 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) ![Icons](doc/grid.png)
@ -217,7 +217,7 @@ Example:
roentgen tile -b 2.361,48.871,2.368,48.875 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 Tile server
----------- -----------

View file

@ -4,7 +4,7 @@
\b {Röntgen} (or \b {Roentgen} when ASCII is preferred) project consists of \b {Röntgen} (or \b {Roentgen} when ASCII is preferred) project consists of
\list \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.} {\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. 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}. 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} \image {doc/grid.png} {Icons}
@ -230,7 +230,7 @@ Example\:
\code {roentgen tile -b 2.361,48.871,2.368,48.875} {bash} \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} \2 {Tile server} {tile-server}

View file

@ -1,3 +1,6 @@
"""
Rectangle that limit space on the map.
"""
import logging import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass

View file

@ -21,7 +21,7 @@ from roentgen.icon import (
from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay from roentgen.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay
from roentgen.point import Point from roentgen.point import Point
from roentgen.scheme import DEFAULT_COLOR, LineStyle, Scheme 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 from roentgen.util import MinMax
# fmt: on # fmt: on
@ -129,17 +129,25 @@ class Constructor:
flinger: Flinger, flinger: Flinger,
scheme: Scheme, scheme: Scheme,
icon_extractor: ShapeExtractor, icon_extractor: ShapeExtractor,
check_level=lambda x: True, options,
mode: str = "normal",
seed: str = "",
) -> None: ) -> None:
self.check_level = check_level
self.mode: str = mode
self.seed: str = seed
self.osm_data: OSMData = osm_data self.osm_data: OSMData = osm_data
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.icon_extractor = icon_extractor 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.points: list[Point] = []
self.figures: list[StyledFigure] = [] self.figures: list[StyledFigure] = []
@ -195,10 +203,10 @@ class Constructor:
return return
center_point, center_coordinates = line_center(outers[0], self.flinger) 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 color: Color
if self.mode == AUTHOR_MODE: if self.options.mode == AUTHOR_MODE:
color = get_user_color(line.user, self.seed) color = get_user_color(line.user, self.options.seed)
else: # self.mode == TIME_MODE else: # self.mode == TIME_MODE
color = get_time_color(line.timestamp, self.osm_data.time) color = get_time_color(line.timestamp, self.osm_data.time)
self.draw_special_mode(inners, line, outers, color) self.draw_special_mode(inners, line, outers, color)
@ -207,7 +215,11 @@ class Constructor:
if not line.tags: if not line.tags:
return 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( self.add_building(
Building(line.tags, inners, outers, self.flinger, self.scheme) Building(line.tags, inners, outers, self.flinger, self.scheme)
) )
@ -276,9 +288,7 @@ class Constructor:
self.points.append(point) self.points.append(point)
def draw_special_mode(self, inners, line, outers, color) -> None: 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] = { style: dict[str, Any] = {
"fill": "none", "fill": "none",
"stroke": color.hex, "stroke": color.hex,
@ -289,9 +299,7 @@ class Constructor:
) )
def construct_relations(self) -> None: 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: for relation_id in self.osm_data.relations:
relation: OSMRelation = self.osm_data.relations[relation_id] relation: OSMRelation = self.osm_data.relations[relation_id]
tags = relation.tags tags = relation.tags
@ -344,13 +352,13 @@ class Constructor:
icon_set: IconSet icon_set: IconSet
draw_outline: bool = True draw_outline: bool = True
if self.mode in [TIME_MODE, AUTHOR_MODE]: if self.options.mode in [TIME_MODE, AUTHOR_MODE]:
if not tags: if not tags:
return return
color: Color = DEFAULT_COLOR color: Color = DEFAULT_COLOR
if self.mode == AUTHOR_MODE: if self.options.mode == AUTHOR_MODE:
color = get_user_color(node.user, self.seed) color = get_user_color(node.user, self.options.seed)
if self.mode == TIME_MODE: if self.options.mode == TIME_MODE:
color = get_time_color(node.timestamp, self.osm_data.time) color = get_time_color(node.timestamp, self.osm_data.time)
dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID) dot = self.icon_extractor.get_shape(DEFAULT_SMALL_SHAPE_ID)
icon_set = IconSet( icon_set = IconSet(
@ -382,3 +390,37 @@ class Constructor:
priority=priority, draw_outline=draw_outline priority=priority, draw_outline=draw_outline
) # fmt: skip ) # fmt: skip
self.points.append(point) 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

View file

@ -17,9 +17,7 @@ DEFAULT_ANGLE: float = np.pi / 30
def degree_to_radian(degree: float) -> float: def degree_to_radian(degree: float) -> float:
""" """Convert value in degrees to radians."""
Convert value in degrees to radians.
"""
return degree / 180 * np.pi return degree / 180 * np.pi

View file

@ -1,3 +1,6 @@
"""
Drawing separate map elements.
"""
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
@ -5,10 +8,10 @@ from pathlib import Path
import numpy as np import numpy as np
import svgwrite import svgwrite
from roentgen.workspace import workspace
from roentgen.icon import ShapeExtractor from roentgen.icon import ShapeExtractor
from roentgen.point import Point from roentgen.point import Point
from roentgen.scheme import LineStyle, Scheme from roentgen.scheme import LineStyle, Scheme
from roentgen.workspace import workspace
def draw_element(options) -> None: def draw_element(options) -> None:

View file

@ -109,6 +109,13 @@ class Building(Figure):
if height: if height:
self.min_height = 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: def draw_shade(self, building_shade, flinger: Flinger) -> None:
"""Draw shade casted by the building.""" """Draw shade casted by the building."""
scale: float = flinger.get_scale() / 3.0 scale: float = flinger.get_scale() / 3.0

View file

@ -5,7 +5,7 @@ from typing import Optional
import numpy as np import numpy as np
from roentgen.util import MinMax from roentgen.boundary_box import BoundaryBox
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
@ -45,7 +45,7 @@ class Flinger:
def __init__( def __init__(
self, self,
geo_boundaries: MinMax, geo_boundaries: BoundaryBox,
scale: float = 18, scale: float = 18,
border: np.array = np.array((0, 0)), border: np.array = np.array((0, 0)),
) -> None: ) -> None:
@ -53,14 +53,14 @@ class Flinger:
:param geo_boundaries: minimum and maximum latitude and longitude :param geo_boundaries: minimum and maximum latitude and longitude
:param scale: OSM zoom level :param scale: OSM zoom level
""" """
self.geo_boundaries: MinMax = geo_boundaries self.geo_boundaries: BoundaryBox = geo_boundaries
self.border = border self.border = border
self.ratio: float = ( self.ratio: float = (
osm_zoom_level_to_pixels_per_meter(scale) * EQUATOR_LENGTH / 360 osm_zoom_level_to_pixels_per_meter(scale) * EQUATOR_LENGTH / 360
) )
self.size: np.array = border * 2 + self.ratio * ( self.size: np.array = border * 2 + self.ratio * (
pseudo_mercator(self.geo_boundaries.max_) pseudo_mercator(self.geo_boundaries.max_())
- pseudo_mercator(self.geo_boundaries.min_) - pseudo_mercator(self.geo_boundaries.min_())
) )
self.pixels_per_meter = osm_zoom_level_to_pixels_per_meter(scale) 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 * ( result: np.array = self.border + self.ratio * (
pseudo_mercator(coordinates) pseudo_mercator(coordinates)
- pseudo_mercator(self.geo_boundaries.min_) - pseudo_mercator(self.geo_boundaries.min_())
) )
# Invert y axis on coordinate plane. # Invert y axis on coordinate plane.

View file

@ -48,9 +48,7 @@ class IconCollection:
icons: list[Icon] = [] icons: list[Icon] = []
def add() -> Icon: def add() -> Icon:
""" """Construct icon and add it to the list."""
Construct icon and add it to the list.
"""
specifications = [ specifications = [
ShapeSpecification.from_structure(x, extractor, scheme) ShapeSpecification.from_structure(x, extractor, scheme)
for x in current_set for x in current_set

View file

@ -118,9 +118,7 @@ class Shape:
def parse_length(text: str) -> float: def parse_length(text: str) -> float:
""" """Parse length from SVG attribute."""
Parse length from SVG attribute.
"""
if text.endswith("px"): if text.endswith("px"):
text = text[:-2] text = text[:-2]
return float(text) return float(text)
@ -319,9 +317,7 @@ class ShapeSpecification:
) )
def is_default(self) -> bool: def is_default(self) -> bool:
""" """Check whether shape is default."""
Check whether shape is default.
"""
return self.shape.id_ == DEFAULT_SHAPE_ID return self.shape.id_ == DEFAULT_SHAPE_ID
def draw( def draw(
@ -389,15 +385,11 @@ class Icon:
shape_specifications: list[ShapeSpecification] shape_specifications: list[ShapeSpecification]
def get_shape_ids(self) -> list[str]: 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] return [x.shape.id_ for x in self.shape_specifications]
def get_names(self) -> list[str]: def get_names(self) -> list[str]:
""" """Get all shape names in the icon."""
Gat all shape names in the icon.
"""
return [ return [
(x.shape.name if x.shape.name else "unknown") (x.shape.name if x.shape.name else "unknown")
for x in self.shape_specifications for x in self.shape_specifications
@ -462,15 +454,11 @@ class Icon:
svg.write(output_file) svg.write(output_file)
def is_default(self) -> bool: def is_default(self) -> bool:
""" """Check whether first shape is default."""
Check whether first shape is default.
"""
return self.shape_specifications[0].is_default() return self.shape_specifications[0].is_default()
def recolor(self, color: Color, white: Optional[Color] = None) -> None: 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: for shape_specification in self.shape_specifications:
if shape_specification.color == Color("white") and white: if shape_specification.color == Color("white") and white:
shape_specification.color = white shape_specification.color = white
@ -480,9 +468,7 @@ class Icon:
def add_specifications( def add_specifications(
self, specifications: list[ShapeSpecification] self, specifications: list[ShapeSpecification]
) -> None: ) -> None:
""" """Add shape specifications to the icon."""
Add shape specifications to the icon.
"""
self.shape_specifications += specifications self.shape_specifications += specifications
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:

View file

@ -1,23 +1,16 @@
""" """
Röntgen entry point. Röntgen entry point.
Author: Sergey Vartanov (me@enzet.ru).
""" """
import argparse import argparse
import logging import logging
import sys import sys
from pathlib import Path 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.ui import parse_options
from roentgen.workspace import Workspace from roentgen.workspace import Workspace
__author__ = "Sergey Vartanov"
def init_scheme(workspace: Workspace) -> Scheme: __email__ = "me@enzet.ru"
"""Initialize default scheme."""
return Scheme(workspace.DEFAULT_SCHEME_PATH)
def main() -> None: def main() -> None:
@ -38,6 +31,8 @@ def main() -> None:
tile.ui(arguments) tile.ui(arguments)
elif arguments.command == "icons": elif arguments.command == "icons":
from roentgen.grid import draw_icons
draw_icons() draw_icons()
elif arguments.command == "mapcss": elif arguments.command == "mapcss":
@ -46,6 +41,8 @@ def main() -> None:
mapcss.ui(arguments) mapcss.ui(arguments)
elif arguments.command == "element": elif arguments.command == "element":
from roentgen.element import draw_element
draw_element(arguments) draw_element(arguments)
elif arguments.command == "server": elif arguments.command == "server":
@ -54,9 +51,10 @@ def main() -> None:
server.ui(arguments) server.ui(arguments)
elif arguments.command == "taginfo": elif arguments.command == "taginfo":
from roentgen.scheme import Scheme
from roentgen.taginfo import write_taginfo_project_file 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__": if __name__ == "__main__":

View file

@ -140,9 +140,7 @@ class MapCSSWriter:
return selector return selector
def write(self, output_file: TextIO) -> None: 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") output_file.write(HEADER + "\n\n")
if self.add_ways: if self.add_ways:
@ -178,9 +176,7 @@ class MapCSSWriter:
def ui(options) -> None: def ui(options) -> None:
""" """Write MapCSS 0.2 scheme."""
Write MapCSS 0.2 scheme.
"""
directory: Path = workspace.get_mapcss_path() directory: Path = workspace.get_mapcss_path()
icons_with_outline_path: Path = workspace.get_mapcss_icons_path() icons_with_outline_path: Path = workspace.get_mapcss_icons_path()

View file

@ -1,10 +1,10 @@
""" """
Simple OpenStreetMap renderer. Simple OpenStreetMap renderer.
""" """
import logging
from pathlib import Path from pathlib import Path
from typing import Any, Iterator from typing import Any, Iterator
import logging
import numpy as np import numpy as np
import svgwrite import svgwrite
from colour import Color from colour import Color
@ -12,17 +12,17 @@ from svgwrite.container import Group
from svgwrite.path import Path as SVGPath from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect from svgwrite.shapes import Rect
from roentgen.icon import ShapeExtractor from roentgen.boundary_box import BoundaryBox
from roentgen.osm_getter import NetworkError, get_osm
from roentgen.constructor import Constructor from roentgen.constructor import Constructor
from roentgen.figure import Road from roentgen.figure import Road
from roentgen.flinger import Flinger 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.osm_reader import OSMData, OSMNode, OSMReader, OverpassReader
from roentgen.point import Occupied from roentgen.point import Occupied
from roentgen.road import Intersection, RoadPart from roentgen.road import Intersection, RoadPart
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.ui import AUTHOR_MODE, BoundaryBox, TIME_MODE, progress_bar from roentgen.ui import AUTHOR_MODE, BuildingMode, TIME_MODE, progress_bar
from roentgen.util import MinMax
from roentgen.workspace import workspace from roentgen.workspace import workspace
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -35,24 +35,15 @@ class Map:
""" """
def __init__( def __init__(
self, self, flinger: Flinger, svg: svgwrite.Drawing, scheme: Scheme, options
flinger: Flinger,
svg: svgwrite.Drawing,
scheme: Scheme,
overlap: int = 12,
mode: str = "normal",
label_mode: str = "main",
) -> None: ) -> None:
self.overlap: int = overlap
self.mode: str = mode
self.label_mode: str = label_mode
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg self.svg: svgwrite.Drawing = svg
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.options = options
self.background_color: Color = self.scheme.get_color("background_color") 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") self.background_color: Color = Color("#111111")
def draw(self, constructor: Constructor) -> None: def draw(self, constructor: Constructor) -> None:
@ -89,11 +80,11 @@ class Map:
# All other points # All other points
if self.overlap == 0: if self.options.overlap == 0:
occupied = None occupied = None
else: else:
occupied = Occupied( 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) 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" steps * 2 + index, steps * 3, step=10, text="Drawing texts"
) )
if ( if (
self.mode not in [TIME_MODE, AUTHOR_MODE] self.options.mode not in [TIME_MODE, AUTHOR_MODE]
and self.label_mode != "no" 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") progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
def draw_buildings(self, constructor: Constructor) -> None: def draw_buildings(self, constructor: Constructor) -> None:
"""Draw buildings: shade, walls, and roof.""" """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 scale: float = self.flinger.get_scale() / 3.0
building_shade: Group = Group(opacity=0.1)
for building in constructor.buildings: for building in constructor.buildings:
building.draw_shade(building_shade, self.flinger) building.draw_shade(building_shade, self.flinger)
self.svg.add(building_shade) self.svg.add(building_shade)
@ -150,9 +148,7 @@ class Map:
def draw_road( def draw_road(
self, road: Road, color: Color, extra_width: float = 0 self, road: Road, color: Color, extra_width: float = 0
) -> None: ) -> None:
""" """Draw road as simple SVG path."""
Draw road as simple SVG path.
"""
self.flinger.get_scale() self.flinger.get_scale()
if road.width is not None: if road.width is not None:
width = road.width width = road.width
@ -172,9 +168,7 @@ class Map:
self.svg.add(path) self.svg.add(path)
def draw_roads(self, roads: Iterator[Road]) -> None: 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]] = {} nodes: dict[OSMNode, set[RoadPart]] = {}
for road in roads: for road in roads:
@ -203,44 +197,6 @@ class Map:
intersection.draw(self.svg, True) 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: def ui(options) -> None:
""" """
Röntgen entry point. Röntgen entry point.
@ -276,27 +232,21 @@ def ui(options) -> None:
min_: np.array min_: np.array
max_: np.array max_: np.array
osm_data: OSMData osm_data: OSMData
view_box: BoundaryBox
if input_file_names[0].name.endswith(".json"): if input_file_names[0].name.endswith(".json"):
reader: OverpassReader = OverpassReader() reader: OverpassReader = OverpassReader()
reader.parse_json_file(input_file_names[0]) reader.parse_json_file(input_file_names[0])
osm_data = reader.osm_data osm_data = reader.osm_data
view_box = MinMax( view_box = boundary_box
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_)
),
)
else: else:
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE] is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE]
osm_reader = OSMReader(is_full=is_full) osm_reader = OSMReader(is_full=is_full)
for file_name in input_file_names: for file_name in input_file_names:
if not file_name.is_file(): if not file_name.is_file():
print(f"Fatal: no such file: {file_name}.") logging.fatal(f"No such file: {file_name}.")
exit(1) exit(1)
osm_reader.parse_osm_file(file_name) osm_reader.parse_osm_file(file_name)
@ -304,10 +254,7 @@ def ui(options) -> None:
osm_data = osm_reader.osm_data osm_data = osm_reader.osm_data
if options.boundary_box: if options.boundary_box:
view_box = MinMax( view_box = boundary_box
np.array((boundary_box.bottom, boundary_box.left)),
np.array((boundary_box.top, boundary_box.right)),
)
else: else:
view_box = osm_data.view_box view_box = osm_data.view_box
@ -321,48 +268,18 @@ def ui(options) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH 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( constructor: Constructor = Constructor(
osm_data, osm_data=osm_data,
flinger, flinger=flinger,
scheme, scheme=scheme,
icon_extractor, icon_extractor=icon_extractor,
check_level, options=options,
options.mode,
options.seed,
) )
constructor.construct() constructor.construct()
painter: Map = Map( painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme, options=options)
overlap=options.overlap,
mode=options.mode,
label_mode=options.label_mode,
flinger=flinger,
svg=svg,
scheme=scheme,
)
painter.draw(constructor) 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: with open(options.output_file_name, "w") as output_file:
svg.write(output_file) svg.write(output_file)

View file

@ -9,7 +9,7 @@ from pathlib import Path
import urllib3 import urllib3
from roentgen.ui import BoundaryBox from roentgen.boundary_box import BoundaryBox
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"

View file

@ -11,6 +11,7 @@ from xml.etree import ElementTree
import numpy as np import numpy as np
from roentgen.boundary_box import BoundaryBox
from roentgen.util import MinMax from roentgen.util import MinMax
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -37,9 +38,7 @@ STAGES_OF_DECAY: list[str] = [
def parse_float(string: str) -> Optional[float]: 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: try:
return float(string) return float(string)
except (TypeError, ValueError): except (TypeError, ValueError):
@ -108,7 +107,7 @@ class OSMNode(Tagged):
super().__init__() super().__init__()
self.id_: Optional[int] = None self.id_: Optional[int] = None
self.coordinates: Optional[np.array] = None self.coordinates: Optional[np.ndarray] = None
self.visible: Optional[str] = None self.visible: Optional[str] = None
self.changeset: Optional[str] = None self.changeset: Optional[str] = None
@ -118,10 +117,8 @@ class OSMNode(Tagged):
@classmethod @classmethod
def from_xml_structure(cls, element, is_full: bool = False) -> "OSMNode": def from_xml_structure(cls, element, is_full: bool = False) -> "OSMNode":
""" """Parse node from OSM XML `<node>` element."""
Parse node from OSM XML `<node>` element. node: "OSMNode" = cls()
"""
node = cls()
attributes = element.attrib attributes = element.attrib
node.id_ = int(attributes["id"]) node.id_ = int(attributes["id"])
node.coordinates = np.array( node.coordinates = np.array(
@ -178,9 +175,7 @@ class OSMWay(Tagged):
@classmethod @classmethod
def from_xml_structure(cls, element, nodes, is_full: bool) -> "OSMWay": def from_xml_structure(cls, element, nodes, is_full: bool) -> "OSMWay":
""" """Parse way from OSM XML `<way>` element."""
Parse way from OSM XML `<way>` element.
"""
way = cls(int(element.attrib["id"])) way = cls(int(element.attrib["id"]))
if is_full: if is_full:
way.visible = element.attrib["visible"] way.visible = element.attrib["visible"]
@ -215,15 +210,11 @@ class OSMWay(Tagged):
return self return self
def is_cycle(self) -> bool: 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] return self.nodes[0] == self.nodes[-1]
def try_to_glue(self, other: "OSMWay") -> Optional["OSMWay"]: 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]: if self.nodes[0] == other.nodes[0]:
return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes) return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes)
if self.nodes[0] == other.nodes[-1]: if self.nodes[0] == other.nodes[-1]:
@ -255,9 +246,7 @@ class OSMRelation(Tagged):
@classmethod @classmethod
def from_xml_structure(cls, element, is_full: bool) -> "OSMRelation": def from_xml_structure(cls, element, is_full: bool) -> "OSMRelation":
""" """Parse relation from OSM XML `<relation>` element."""
Parse relation from OSM XML `<relation>` element.
"""
attributes = element.attrib attributes = element.attrib
relation = cls(int(attributes["id"])) relation = cls(int(attributes["id"]))
if is_full: if is_full:
@ -321,33 +310,24 @@ class OSMData:
self.authors: set[str] = set() self.authors: set[str] = set()
self.time: MinMax = MinMax() self.time: MinMax = MinMax()
self.boundary_box: list[MinMax] = [MinMax(), MinMax()] self.view_box: Optional[BoundaryBox] = None
self.view_box = None
def add_node(self, node: OSMNode) -> 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 self.nodes[node.id_] = node
if node.user: if node.user:
self.authors.add(node.user) self.authors.add(node.user)
self.time.update(node.timestamp) 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: def add_way(self, way: OSMWay) -> None:
""" """Add way and update map parameters."""
Add way and update map parameters.
"""
self.ways[way.id_] = way self.ways[way.id_] = way
if way.user: if way.user:
self.authors.add(way.user) self.authors.add(way.user)
self.time.update(way.timestamp) self.time.update(way.timestamp)
def add_relation(self, relation: OSMRelation) -> None: def add_relation(self, relation: OSMRelation) -> None:
""" """Add relation and update map parameters."""
Add relation and update map parameters.
"""
self.relations[relation.id_] = relation self.relations[relation.id_] = relation
@ -362,9 +342,7 @@ class OverpassReader:
self.osm_data = OSMData() self.osm_data = OSMData()
def parse_json_file(self, file_name: Path) -> 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: with file_name.open() as input_file:
structure = json.load(input_file) structure = json.load(input_file)
@ -460,15 +438,11 @@ class OSMReader:
return self.osm_data return self.osm_data
def parse_bounds(self, element) -> None: def parse_bounds(self, element) -> None:
""" """Parse view box from XML element."""
Parse view box from XML element.
"""
attributes = element.attrib attributes = element.attrib
self.osm_data.view_box = MinMax( self.osm_data.view_box = BoundaryBox(
np.array( float(attributes["minlon"]),
(float(attributes["minlat"]), float(attributes["minlon"])) float(attributes["minlat"]),
), float(attributes["maxlon"]),
np.array( float(attributes["maxlat"]),
(float(attributes["maxlat"]), float(attributes["maxlon"]))
),
) )

View file

@ -30,17 +30,13 @@ class Occupied:
self.overlap: float = overlap self.overlap: float = overlap
def check(self, point: np.array) -> bool: 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: if 0 <= point[0] < self.width and 0 <= point[1] < self.height:
return self.matrix[point[0], point[1]] return self.matrix[point[0], point[1]]
return True return True
def register(self, point) -> None: 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: if 0 <= point[0] < self.width and 0 <= point[1] < self.height:
self.matrix[point[0], point[1]] = True self.matrix[point[0], point[1]] = True
assert self.matrix[point[0], point[1]] assert self.matrix[point[0], point[1]]
@ -86,9 +82,7 @@ class Point(Tagged):
def draw_main_shapes( def draw_main_shapes(
self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None
) -> 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] keys_left = [x for x in self.tags.keys() if x not in self.processed]
if ( if (
self.icon_set.main_icon.is_default() self.icon_set.main_icon.is_default()
@ -107,9 +101,7 @@ class Point(Tagged):
def draw_extra_shapes( def draw_extra_shapes(
self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None
) -> None: ) -> None:
""" """Draw secondary shapes."""
Draw secondary shapes.
"""
if not self.icon_set.extra_icons or not self.main_icon_painted: if not self.icon_set.extra_icons or not self.main_icon_painted:
return return
@ -141,9 +133,7 @@ class Point(Tagged):
occupied, occupied,
tags: Optional[dict[str, str]] = None, tags: Optional[dict[str, str]] = None,
) -> bool: ) -> 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. # Down-cast floats to integers to make icons pixel-perfect.
position = list(map(int, position)) position = list(map(int, position))
@ -173,9 +163,7 @@ class Point(Tagged):
occupied: Optional[Occupied] = None, occupied: Optional[Occupied] = None,
label_mode: str = "main", label_mode: str = "main",
) -> None: ) -> None:
""" """Draw all labels."""
Draw all labels.
"""
labels: list[Label] labels: list[Label]
if label_mode == "main": if label_mode == "main":

View file

@ -81,14 +81,13 @@ class RoadPart:
self.left_outer = None self.left_outer = None
self.right_outer = None self.right_outer = None
self.point_a = None self.point_a = None
self.point_middle = None
@classmethod @classmethod
def from_nodes( def from_nodes(
cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, road, scale cls, node_1: OSMNode, node_2: OSMNode, flinger: Flinger, road, scale
) -> "RoadPart": ) -> "RoadPart":
""" """Construct road part from OSM nodes."""
Construct road part from OSM nodes.
"""
lanes = [Lane(road.width / road.lanes)] * road.lanes lanes = [Lane(road.width / road.lanes)] * road.lanes
return cls( return cls(
@ -99,9 +98,7 @@ class RoadPart:
) )
def update(self) -> None: def update(self) -> None:
""" """Compute additional points."""
Compute additional points.
"""
if self.left_connection is not None: if self.left_connection is not None:
self.right_projection = ( self.right_projection = (
self.left_connection + self.right_vector - self.left_vector self.left_connection + self.right_vector - self.left_vector
@ -136,15 +133,11 @@ class RoadPart:
self.point_a = self.point_middle self.point_a = self.point_middle
def get_angle(self) -> float: 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) return compute_angle(self.point_2 - self.point_1)
def draw_normal(self, drawing: svgwrite.Drawing): def draw_normal(self, drawing: svgwrite.Drawing):
""" """Draw some debug lines."""
Draw some debug lines.
"""
line = drawing.path( line = drawing.path(
("M", self.point_1, "L", self.point_2), ("M", self.point_1, "L", self.point_2),
fill="none", fill="none",
@ -154,9 +147,7 @@ class RoadPart:
drawing.add(line) drawing.add(line)
def draw_debug(self, drawing: svgwrite.Drawing): def draw_debug(self, drawing: svgwrite.Drawing):
""" """Draw some debug lines."""
Draw some debug lines.
"""
line = drawing.path( line = drawing.path(
("M", self.point_1, "L", self.point_2), ("M", self.point_1, "L", self.point_2),
fill="none", fill="none",
@ -237,9 +228,7 @@ class RoadPart:
# self.draw_entrance(drawing, True) # self.draw_entrance(drawing, True)
def draw(self, drawing: svgwrite.Drawing): def draw(self, drawing: svgwrite.Drawing):
""" """Draw road part."""
Draw road part.
"""
if self.left_connection is not None: if self.left_connection is not None:
path_commands = [ path_commands = [
"M", self.point_2 + self.right_vector, "M", self.point_2 + self.right_vector,
@ -251,9 +240,7 @@ class RoadPart:
drawing.add(drawing.path(path_commands, fill="#CCCCCC")) drawing.add(drawing.path(path_commands, fill="#CCCCCC"))
def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool = False): def draw_entrance(self, drawing: svgwrite.Drawing, is_debug: bool = False):
""" """Draw intersection entrance part."""
Draw intersection entrance part.
"""
if ( if (
self.left_connection is not None self.left_connection is not None
and self.right_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")) drawing.add(drawing.path(path_commands, fill="#88FF88"))
def draw_lanes(self, drawing: svgwrite.Drawing, scale: float): def draw_lanes(self, drawing: svgwrite.Drawing, scale: float):
""" """Draw lane delimiters."""
Draw lane delimiters.
"""
for lane in self.lanes: for lane in self.lanes:
shift = self.right_vector - self.turned * lane.get_width(scale) shift = self.right_vector - self.turned * lane.get_width(scale)
path = drawing.path( path = drawing.path(
@ -340,9 +325,7 @@ class Intersection:
part_2.update() part_2.update()
def draw(self, drawing: svgwrite.Drawing, is_debug: bool = False) -> None: 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"] inner_commands = ["M"]
for part in self.parts: for part in self.parts:
inner_commands += [part.left_connection, "L"] inner_commands += [part.left_connection, "L"]

View file

@ -413,12 +413,6 @@ class Scheme:
if main_icon and color: if main_icon and color:
main_icon.recolor(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) default_shape = extractor.get_shape(DEFAULT_SHAPE_ID)
if not main_icon: if not main_icon:
main_icon = Icon([ShapeSpecification(default_shape)]) main_icon = Icon([ShapeSpecification(default_shape)])

View file

@ -1,5 +1,5 @@
""" """
Röntgen tile server for sloppy maps. Röntgen tile server for slippy maps.
""" """
import logging import logging
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
@ -15,13 +15,14 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
class Handler(SimpleHTTPRequestHandler): class _Handler(SimpleHTTPRequestHandler):
""" """
HTTP request handler that process sloppy map tile requests. HTTP request handler that process sloppy map tile requests.
""" """
cache: Path = Path("cache") cache: Path = Path("cache")
update_cache: bool = False update_cache: bool = False
options = None
def __init__( def __init__(
self, request: bytes, client_address: tuple[str, int], server self, request: bytes, client_address: tuple[str, int], server
@ -33,22 +34,25 @@ class Handler(SimpleHTTPRequestHandler):
parts: list[str] = self.path.split("/") parts: list[str] = self.path.split("/")
if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"): if not (len(parts) == 5 and not parts[0] and parts[1] == "tiles"):
return return
zoom: int = int(parts[2])
scale: int = int(parts[2])
x: int = int(parts[3]) x: int = int(parts[3])
y: int = int(parts[4]) y: int = int(parts[4])
tile_path: Path = workspace.get_tile_path() 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: 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 png_path.exists():
if not svg_path.exists(): if not svg_path.exists():
tile = Tile(x, y, zoom) tile = Tile(x, y, scale)
tile.draw(tile_path, self.cache) tile.draw(tile_path, self.cache, self.options)
with svg_path.open() as input_file: with svg_path.open() as input_file:
cairosvg.svg2png( cairosvg.svg2png(
file_obj=input_file, write_to=str(png_path) file_obj=input_file, write_to=str(png_path)
) )
logging.info(f"SVG file is rasterized to {png_path}.") logging.info(f"SVG file is rasterized to {png_path}.")
if png_path.exists(): if png_path.exists():
with png_path.open("rb") as input_file: with png_path.open("rb") as input_file:
self.send_response(200) self.send_response(200)
@ -63,11 +67,12 @@ def ui(options) -> None:
server: Optional[HTTPServer] = None server: Optional[HTTPServer] = None
try: try:
port: int = 8080 port: int = 8080
handler = Handler handler = _Handler
handler.cache = Path(options.cache) handler.cache = Path(options.cache)
handler.options = options
server: HTTPServer = HTTPServer(("", port), handler) server: HTTPServer = HTTPServer(("", port), handler)
server.serve_forever()
logging.info(f"Server started on port {port}.") logging.info(f"Server started on port {port}.")
server.serve_forever()
finally: finally:
if server: if server:
server.socket.close() server.socket.close()

View file

@ -75,16 +75,12 @@ def format_voltage(value: str) -> str:
def format_frequency(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} " return f"{value} "
def get_text(tags: dict[str, Any], processed: set[str]) -> list[Label]: 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] = [] texts: list[Label] = []
values: list[str] = [] values: list[str] = []

View file

@ -21,8 +21,7 @@ from roentgen.mapper import Map
from roentgen.osm_getter import NetworkError, get_osm from roentgen.osm_getter import NetworkError, get_osm
from roentgen.osm_reader import OSMData, OSMReader from roentgen.osm_reader import OSMData, OSMReader
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.ui import BoundaryBox from roentgen.boundary_box import BoundaryBox
from roentgen.util import MinMax
from roentgen.workspace import workspace from roentgen.workspace import workspace
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -116,36 +115,33 @@ class Tile:
f"https://tile.openstreetmap.org/{self.scale}/{self.x}/{self.y}.png" 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. Draw tile to SVG and PNG files.
:param directory_name: output directory to storing tiles :param directory_name: output directory to storing tiles
:param cache_path: directory to store SVG and PNG tiles :param cache_path: directory to store SVG and PNG tiles
:param options: drawing configuration
""" """
try: try:
osm_data: OSMData = self.load_osm_data(cache_path) osm_data: OSMData = self.load_osm_data(cache_path)
except NetworkError as e: except NetworkError as e:
raise NetworkError(f"Map is not loaded. {e.message}") 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( def draw_with_osm_data(
self, osm_data: OSMData, directory_name: Path self, osm_data: OSMData, directory_name: Path, options
) -> None: ) -> None:
"""Draw SVG and PNG tile using OpenStreetMap data.""" """Draw SVG and PNG tile using OpenStreetMap data."""
latitude_1, longitude_1 = self.get_coordinates() top, left = self.get_coordinates()
latitude_2, longitude_2 = Tile( bottom, right = Tile(
self.x + 1, self.y + 1, self.scale self.x + 1, self.y + 1, self.scale
).get_coordinates() ).get_coordinates()
min_: np.array = np.array( flinger: Flinger = Flinger(
(min(latitude_1, latitude_2), min(longitude_1, longitude_2)) 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 size: np.array = flinger.size
output_file_name: Path = self.get_file_name(directory_name) output_file_name: Path = self.get_file_name(directory_name)
@ -158,11 +154,13 @@ class Tile:
) )
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, scheme, icon_extractor osm_data, flinger, scheme, icon_extractor, options
) )
constructor.construct() 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) painter.draw(constructor)
with output_file_name.open("w") as output_file: 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) 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 Draw set of tiles as SVG file separately and rasterize them into a set
of PNG files with cairosvg. of PNG files with cairosvg.
:param directory: directory for tiles :param directory: directory for tiles
:param cache_path: directory for temporary OSM files :param cache_path: directory for temporary OSM files
:param options: drawing configuration
""" """
cache_file_path: Path = ( cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm" cache_path / f"{self.boundary_box.get_format()}.osm"
@ -234,7 +235,7 @@ class Tiles:
for tile in self.tiles: for tile in self.tiles:
file_path: Path = tile.get_file_name(directory) file_path: Path = tile.get_file_name(directory)
if not file_path.exists(): if not file_path.exists():
tile.draw_with_osm_data(osm_data, directory) tile.draw_with_osm_data(osm_data, directory, options)
else: else:
logging.debug(f"File {file_path} already exists.") logging.debug(f"File {file_path} already exists.")
@ -252,18 +253,19 @@ class Tiles:
"""Check whether all tiles are drawn.""" """Check whether all tiles are drawn."""
return all(x.exists(directory_name) for x in self.tiles) 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 Draw one PNG image with all tiles and split it into a set of separate
PNG file with Pillow. PNG file with Pillow.
:param directory: directory for tiles :param directory: directory for tiles
:param cache_path: directory for temporary OSM files :param cache_path: directory for temporary OSM files
:param options: drawing configuration
""" """
if self.tiles_exist(directory): if self.tiles_exist(directory):
return return
self.draw_image(cache_path) self.draw_image(cache_path, options)
input_path: Path = self.get_file_path(cache_path).with_suffix(".png") input_path: Path = self.get_file_path(cache_path).with_suffix(".png")
with input_path.open("rb") as input_file: with input_path.open("rb") as input_file:
@ -288,11 +290,12 @@ class Tiles:
"""Get path of the output SVG file.""" """Get path of the output SVG file."""
return cache_path / f"{self.boundary_box.get_format()}_{self.scale}.svg" 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. Draw all tiles as one picture.
:param cache_path: directory for temporary SVG file and OSM files :param cache_path: directory for temporary SVG file and OSM files
:param options: drawing configuration
""" """
output_path: Path = self.get_file_path(cache_path) output_path: Path = self.get_file_path(cache_path)
@ -303,27 +306,29 @@ class Tiles:
get_osm(self.boundary_box, cache_file_path) get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path) osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
latitude_2, longitude_1 = self.tile_1.get_coordinates() top, left = self.tile_1.get_coordinates()
latitude_1, longitude_2 = Tile( bottom, right = Tile(
self.tile_2.x + 1, self.tile_2.y + 1, self.scale self.tile_2.x + 1, self.tile_2.y + 1, self.scale
).get_coordinates() ).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( extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH) scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, scheme, extractor osm_data, flinger, scheme, extractor, options=options
) )
constructor.construct() constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing( svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size 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) map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...") 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) tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale)
try: try:
tile.draw(directory, Path(options.cache)) tile.draw(directory, Path(options.cache), options)
except NetworkError as e: except NetworkError as e:
logging.fatal(e.message) logging.fatal(e.message)
elif options.tile: elif options.tile:
scale, x, y = map(int, options.tile.split("/")) scale, x, y = map(int, options.tile.split("/"))
tile: Tile = Tile(x, y, scale) tile: Tile = Tile(x, y, scale)
tile.draw(directory, Path(options.cache)) tile.draw(directory, Path(options.cache), options)
elif options.boundary_box: elif options.boundary_box:
boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text( boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text(
options.boundary_box options.boundary_box
@ -365,7 +370,7 @@ def ui(options) -> None:
if boundary_box is None: if boundary_box is None:
sys.exit(1) sys.exit(1)
tiles: Tiles = Tiles.from_boundary_box(boundary_box, options.scale) tiles: Tiles = Tiles.from_boundary_box(boundary_box, options.scale)
tiles.draw(directory, Path(options.cache)) tiles.draw(directory, Path(options.cache), options)
else: else:
logging.fatal( logging.fatal(
"Specify either --coordinates, --boundary-box, or --tile." "Specify either --coordinates, --boundary-box, or --tile."

View file

@ -2,16 +2,12 @@
Command-line user interface. Command-line user interface.
""" """
import argparse import argparse
import re
import sys import sys
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
import logging from enum import Enum
from dataclasses import dataclass
import numpy as np
from roentgen.osm_reader import STAGES_OF_DECAY from roentgen.osm_reader import STAGES_OF_DECAY
@ -21,8 +17,15 @@ BOXES_LENGTH: int = len(BOXES)
AUTHOR_MODE: str = "author" AUTHOR_MODE: str = "author"
TIME_MODE: str = "time" 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: def parse_options(args) -> argparse.Namespace:
@ -32,8 +35,14 @@ def parse_options(args) -> argparse.Namespace:
) )
subparser = parser.add_subparsers(dest="command") subparser = parser.add_subparsers(dest="command")
add_render_arguments(subparser.add_parser("render")) tile_parser = subparser.add_parser("tile")
add_tile_arguments(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_server_arguments(subparser.add_parser("server"))
add_element_arguments(subparser.add_parser("element")) add_element_arguments(subparser.add_parser("element"))
add_mapcss_arguments(subparser.add_parser("mapcss")) add_mapcss_arguments(subparser.add_parser("mapcss"))
@ -46,13 +55,36 @@ def parse_options(args) -> argparse.Namespace:
return arguments return arguments
def add_tile_arguments(parser: argparse.ArgumentParser) -> None: def add_map_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for tile command.""" """Add map-specific arguments."""
parser.add_argument( parser.add_argument(
"-c", "--buildings",
"--coordinates", metavar="<mode>",
metavar="<latitude>,<longitude>", default="flat",
help="coordinates of any location inside the tile", 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="<string>",
)
parser.add_argument(
"--overlap",
dest="overlap",
default=12,
type=int,
help="how many pixels should be left around icons and text",
metavar="<integer>",
)
parser.add_argument(
"--labels",
help="label drawing mode: `no`, `main`, or `all`",
dest="label_mode",
default="main",
metavar="<string>",
) )
parser.add_argument( parser.add_argument(
"-s", "-s",
@ -62,6 +94,21 @@ def add_tile_arguments(parser: argparse.ArgumentParser) -> None:
help="OSM zoom level", help="OSM zoom level",
default=18, 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="<latitude>,<longitude>",
help="coordinates of any location inside the tile",
)
parser.add_argument( parser.add_argument(
"-t", "-t",
"--tile", "--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 " help="geo boundary box; if first value is negative, enclose the value "
"with quotes and use space before `-`", "with quotes and use space before `-`",
) )
parser.add_argument(
"-s",
"--scale",
metavar="<float>",
help="OSM zoom level (may not be integer)",
default=18,
type=float,
)
parser.add_argument( parser.add_argument(
"--cache", "--cache",
help="path for temporary OSM files", help="path for temporary OSM files",
default="cache", default="cache",
metavar="<path>", metavar="<path>",
) )
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( parser.add_argument(
"--seed", "--seed",
default="", default="",
help="seed for random", help="seed for random",
) metavar="<string>",
parser.add_argument(
"--level",
default=None,
help="display only this floor level",
) )
@ -219,91 +236,3 @@ def progress_bar(
f"{int(length - fill_length - 1) * ' '}{text}" f"{int(length - fill_length - 1) * ' '}{text}"
) )
sys.stdout.write("\033[F") 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
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2> or simply
<left>,<bottom>,<right>,<top>.
"""
boundary_box = boundary_box.replace(" ", "")
matcher = re.match(
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
+ "(?P<right>[0-9.-]*),(?P<top>[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:
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are
rounded to three digits after comma.
"""
return (
f"{self.left:.3f},{self.bottom:.3f},{self.right:.3f},{self.top:.3f}"
)

View file

@ -24,9 +24,7 @@ def compute_angle(vector: np.array):
def turn_by_angle(vector: np.array, angle: float): def turn_by_angle(vector: np.array, angle: float):
""" """Turn vector by an angle."""
Turn vector by an angle.
"""
return np.array( return np.array(
( (
vector[0] * np.cos(angle) - vector[1] * np.sin(angle), 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: 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) return vector / np.linalg.norm(vector)
class Line: class Line:
""" """Infinity line: Ax + By + C = 0."""
Infinity line: Ax + By + C = 0.
"""
def __init__(self, start: np.array, end: np.array) -> None: def __init__(self, start: np.array, end: np.array) -> None:
# if start.near(end): # if start.near(end):
@ -63,15 +57,11 @@ class Line:
self.c -= self.a * shift.x + self.b * shift.y self.c -= self.a * shift.x + self.b * shift.y
def is_parallel(self, other: "Line") -> bool: 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) return np.allclose(other.a * self.b - self.a * other.b, 0)
def get_intersection_point(self, other: "Line") -> np.array: 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: if other.a * self.b - self.a * other.b == 0:
return np.array((0, 0)) return np.array((0, 0))

View file

@ -689,25 +689,79 @@ node_icons:
- tags: {man_made: mast, tower:construction: lattice_guyed} - tags: {man_made: mast, tower:construction: lattice_guyed}
shapes: [lattice_guyed] shapes: [lattice_guyed]
- tags: {man_made: mast, tower:type: lighting} - 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} - tags: {man_made: mast, tower:type: communication}
shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] shapes:
- tags: {man_made: mast, tower:type: lighting, tower:construction: guyed_tube} - tube
shapes: [tube_guyed, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - {shape: wave_left, offset: [-3, -3]}
- tags: {man_made: mast, tower:type: communication, tower:construction: guyed_tube} - {shape: wave_right, offset: [3, -3]}
shapes: [tube_guyed, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - tags:
- tags: {man_made: mast, tower:type: lighting, tower:construction: freestanding} man_made: mast
shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] tower:type: lighting
- tags: {man_made: mast, tower:type: communication, tower:construction: freestanding} tower:construction: guyed_tube
shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] shapes:
- tags: {man_made: mast, tower:type: lighting, tower:construction: guyed_lattice} - tube_guyed
shapes: [lattice_guyed, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - {shape: light_left, offset: [-3, -3]}
- tags: {man_made: mast, tower:type: communication, tower:construction: guyed_lattice} - {shape: light_right, offset: [3, -3]}
shapes: [lattice_guyed, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] - tags:
- tags: {man_made: mast, tower:type: lighting, tower:construction: lattice} man_made: mast
shapes: [lattice, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] tower:type: communication
- tags: {man_made: mast, tower:type: communication, tower:construction: lattice} tower:construction: guyed_tube
shapes: [lattice, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] 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} - tags: {man_made: tower, tower:construction: guyed_tube}
shapes: [tube_guyed] shapes: [tube_guyed]
@ -718,25 +772,79 @@ node_icons:
- tags: {man_made: tower, tower:construction: lattice_guyed} - tags: {man_made: tower, tower:construction: lattice_guyed}
shapes: [lattice_guyed] shapes: [lattice_guyed]
- tags: {man_made: tower, tower:type: lighting} - 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} - tags: {man_made: tower, tower:type: communication}
shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] shapes:
- tags: {man_made: tower, tower:type: lighting, tower:construction: guyed_tube} - tube
shapes: [tube_guyed, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] - {shape: wave_left, offset: [-3, -3]}
- tags: {man_made: tower, tower:type: communication, tower:construction: guyed_tube} - {shape: wave_right, offset: [3, -3]}
shapes: [tube_guyed, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] - tags:
- tags: {man_made: tower, tower:type: lighting, tower:construction: freestanding} man_made: tower
shapes: [tube, {shape: light_left, offset: [-3, -3]}, {shape: light_right, offset: [3, -3]}] tower:type: lighting
- tags: {man_made: tower, tower:type: communication, tower:construction: freestanding} tower:construction: guyed_tube
shapes: [tube, {shape: wave_left, offset: [-3, -3]}, {shape: wave_right, offset: [3, -3]}] shapes:
- tags: {man_made: tower, tower:type: lighting, tower:construction: guyed_lattice} - tube_guyed
shapes: [lattice_guyed, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] - {shape: light_left, offset: [-3, -3]}
- tags: {man_made: tower, tower:type: communication, tower:construction: guyed_lattice} - {shape: light_right, offset: [3, -3]}
shapes: [lattice_guyed, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] - tags:
- tags: {man_made: tower, tower:type: lighting, tower:construction: lattice} man_made: tower
shapes: [lattice, {shape: light_left, offset: [-4, -3]}, {shape: light_right, offset: [3, -3]}] tower:type: communication
- tags: {man_made: tower, tower:type: communication, tower:construction: lattice} tower:construction: guyed_tube
shapes: [lattice, {shape: wave_left, offset: [-4, -3]}, {shape: wave_right, offset: [3, -3]}] 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"} - tags: {communication:mobile_phone: "yes"}
add_shapes: [phone] add_shapes: [phone]
@ -873,7 +981,9 @@ node_icons:
- group: "Entrances" - group: "Entrances"
tags: tags:
- tags: {amenity: parking_entrance} - 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} - tags: {amenity: parking_entrance, parking: underground}
shapes: [{shape: p, offset: [-1, 0]}, {shape: arrow_down, offset: [4, 5]}] shapes: [{shape: p, offset: [-1, 0]}, {shape: arrow_down, offset: [4, 5]}]
- tags: {amenity: parking_entrance, parking: multi-storey} - tags: {amenity: parking_entrance, parking: multi-storey}

View file

@ -1,13 +1,14 @@
""" """
Test boundary box. Test boundary box.
""" """
from roentgen.ui import BoundaryBox from roentgen.boundary_box import BoundaryBox
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
def test_round_zero_coordinates() -> None: def test_round_zero_coordinates() -> None:
"""Test rounding for zero coordinates."""
assert ( assert (
BoundaryBox(0, 0, 0, 0).round().get_format() BoundaryBox(0, 0, 0, 0).round().get_format()
== "-0.001,-0.001,0.001,0.001" == "-0.001,-0.001,0.001,0.001"
@ -15,6 +16,7 @@ def test_round_zero_coordinates() -> None:
def test_round_coordinates() -> None: def test_round_coordinates() -> None:
"""Test rounding coordinates."""
box: BoundaryBox = BoundaryBox( box: BoundaryBox = BoundaryBox(
10.067596435546875, 10.067596435546875,
46.094186149226466, 46.094186149226466,