mirror of
https://github.com/enzet/map-machine.git
synced 2025-06-03 11:21:49 +02:00
Add options to tile generation.
This commit is contained in:
parent
163fe4060b
commit
fe2714c927
26 changed files with 448 additions and 524 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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)).
|
||||
|
||||

|
||||
|
||||
|
@ -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
|
||||
-----------
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
Rectangle that limit space on the map.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 `<node>` element.
|
||||
"""
|
||||
node = cls()
|
||||
"""Parse node from OSM XML `<node>` 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 `<way>` element.
|
||||
"""
|
||||
"""Parse way from OSM XML `<way>` 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 `<relation>` element.
|
||||
"""
|
||||
"""Parse relation from OSM XML `<relation>` 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"]),
|
||||
)
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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] = []
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
|
197
roentgen/ui.py
197
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="<latitude>,<longitude>",
|
||||
help="coordinates of any location inside the tile",
|
||||
"--buildings",
|
||||
metavar="<mode>",
|
||||
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="<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(
|
||||
"-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="<latitude>,<longitude>",
|
||||
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="<float>",
|
||||
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="<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(
|
||||
"--seed",
|
||||
default="",
|
||||
help="seed for random",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--level",
|
||||
default=None,
|
||||
help="display only this floor level",
|
||||
metavar="<string>",
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
<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}"
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue