Get use of map configuration.

This commit is contained in:
Sergey Vartanov 2021-08-27 00:40:00 +03:00
parent 56fdf9709e
commit 4c5209dabc
15 changed files with 160 additions and 130 deletions

View file

@ -36,7 +36,7 @@ Röntgen features\:
\3 {Isometric building shapes} {levels} \3 {Isometric building shapes} {levels}
Isometric shapes for walls and shade in proportion to \osm {building:levels}, \osm {building:min_level}, \osm {height} and \osm {min_height} values. With \m {--buildings isometric} or \m {--buildings isometric-no-parts} (not set by default), buildings are drawn using isometric shapes for walls and shade in proportion to \osm {building:levels}, \osm {building:min_level}, \osm {height} and \osm {min_height} values.
\image {doc/buildings.png} {3D buildings} \image {doc/buildings.png} {3D buildings}
@ -265,5 +265,5 @@ To enable / disable Röntgen map paint style go to \kbd {View} → \kbd {Map Pai
Example of using Röntgen icons on top of Mapnik style. Map Paint Styles\: Example of using Röntgen icons on top of Mapnik style. Map Paint Styles\:
\list \list
{✓ Mapnik} {✓ Mapnik (true)}
{✓ Röntgen} {✓ Röntgen}

View file

@ -7,6 +7,9 @@ from dataclasses import dataclass
import numpy as np import numpy as np
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
LATITUDE_MAX_DIFFERENCE: float = 0.5 LATITUDE_MAX_DIFFERENCE: float = 0.5
LONGITUDE_MAX_DIFFERENCE: float = 0.5 LONGITUDE_MAX_DIFFERENCE: float = 0.5

View file

@ -1,7 +1,7 @@
""" """
Color utility. Color utility.
""" """
from typing import Any, List from typing import Any
from colour import Color from colour import Color
@ -22,7 +22,7 @@ def is_bright(color: Color) -> bool:
def get_gradient_color( def get_gradient_color(
value: Any, bounds: MinMax, colors: List[Color] value: Any, bounds: MinMax, colors: list[Color]
) -> Color: ) -> Color:
""" """
Get color from the color scale for the value. Get color from the color scale for the value.
@ -32,7 +32,7 @@ def get_gradient_color(
:param colors: color scale :param colors: color scale
""" """
color_length: int = len(colors) - 1 color_length: int = len(colors) - 1
scale: List[Color] = colors + [Color("black")] scale: list[Color] = colors + [Color("black")]
range_coefficient: float = ( range_coefficient: float = (
0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta() 0 if bounds.is_empty() else (value - bounds.min_) / bounds.delta()

View file

@ -13,6 +13,7 @@ from roentgen import ui
from roentgen.color import get_gradient_color from roentgen.color import get_gradient_color
from roentgen.figure import Building, DirectionSector, Road, StyledFigure, Tree from roentgen.figure import Building, DirectionSector, Road, StyledFigure, Tree
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.map_configuration import DrawingMode, MapConfiguration
# fmt: off # fmt: off
from roentgen.icon import ( from roentgen.icon import (
@ -21,9 +22,8 @@ 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, BuildingMode, TIME_MODE from roentgen.ui import BuildingMode
from roentgen.util import MinMax from roentgen.util import MinMax
# fmt: on # fmt: on
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -113,7 +113,7 @@ def glue(ways: list[OSMWay]) -> list[list[OSMNode]]:
return result return result
def is_cycle(nodes) -> bool: def is_cycle(nodes: list[OSMNode]) -> bool:
"""Is way a cycle way or an area boundary.""" """Is way a cycle way or an area boundary."""
return nodes[0] == nodes[-1] return nodes[0] == nodes[-1]
@ -129,22 +129,22 @@ class Constructor:
flinger: Flinger, flinger: Flinger,
scheme: Scheme, scheme: Scheme,
icon_extractor: ShapeExtractor, icon_extractor: ShapeExtractor,
options, configuration: MapConfiguration,
) -> None: ) -> None:
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 self.configuration = configuration
if options.level: if self.configuration.level:
if options.level == "overground": if self.configuration.level == "overground":
self.check_level = check_level_overground self.check_level = check_level_overground
elif options.level == "underground": elif self.configuration.level == "underground":
self.check_level = lambda x: not check_level_overground(x) self.check_level = lambda x: not check_level_overground(x)
else: else:
self.check_level = lambda x: not check_level_number( self.check_level = lambda x: not check_level_number(
x, float(options.level) x, float(self.configuration.level)
) )
else: else:
self.check_level = lambda x: True self.check_level = lambda x: True
@ -203,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.options.mode in [AUTHOR_MODE, TIME_MODE]: if self.configuration.is_wireframe():
color: Color color: Color
if self.options.mode == AUTHOR_MODE: if self.configuration.drawing_mode == DrawingMode.AUTHOR:
color = get_user_color(line.user, self.options.seed) color = get_user_color(line.user, self.configuration.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)
@ -215,7 +215,7 @@ class Constructor:
if not line.tags: if not line.tags:
return return
building_mode: BuildingMode = BuildingMode(self.options.buildings) building_mode: BuildingMode = self.configuration.building_mode
if "building" in line.tags or ( if "building" in line.tags or (
building_mode == BuildingMode.ISOMETRIC building_mode == BuildingMode.ISOMETRIC
and "building:part" in line.tags and "building:part" in line.tags
@ -352,13 +352,13 @@ class Constructor:
icon_set: IconSet icon_set: IconSet
draw_outline: bool = True draw_outline: bool = True
if self.options.mode in [TIME_MODE, AUTHOR_MODE]: if self.configuration.is_wireframe():
if not tags: if not tags:
return return
color: Color = DEFAULT_COLOR color: Color = DEFAULT_COLOR
if self.options.mode == AUTHOR_MODE: if self.configuration.drawing_mode == DrawingMode.AUTHOR:
color = get_user_color(node.user, self.options.seed) color = get_user_color(node.user, self.configuration.seed)
if self.options.mode == TIME_MODE: if self.configuration.drawing_mode == DrawingMode.TIME:
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(

View file

@ -3,7 +3,7 @@ Drawing utility.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional, Union from typing import Optional, Union
import cairo import cairo
import numpy as np import numpy as np
@ -73,7 +73,7 @@ class Drawing:
"""Draw rectangle.""" """Draw rectangle."""
raise NotImplementedError raise NotImplementedError
def line(self, points: List[np.ndarray], style: Style) -> None: def line(self, points: list[np.ndarray], style: Style) -> None:
"""Draw line.""" """Draw line."""
raise NotImplementedError raise NotImplementedError
@ -111,7 +111,7 @@ class SVGDrawing(Drawing):
style.update_svg_element(rectangle) style.update_svg_element(rectangle)
self.image.add(rectangle) self.image.add(rectangle)
def line(self, points: List[np.ndarray], style: Style) -> None: def line(self, points: list[np.ndarray], style: Style) -> None:
"""Draw line.""" """Draw line."""
commands: PathCommands = ["M"] commands: PathCommands = ["M"]
for point in points: for point in points:
@ -159,7 +159,7 @@ class PNGDrawing(Drawing):
self.context.rectangle(point_1[0], point_1[1], size[0], size[1]) self.context.rectangle(point_1[0], point_1[1], size[0], size[1])
style.draw_png_stroke(self.context) style.draw_png_stroke(self.context)
def line(self, points: List[np.ndarray], style: Style) -> None: def line(self, points: list[np.ndarray], style: Style) -> None:
"""Draw line.""" """Draw line."""
if style.fill is not None: if style.fill is not None:
self.context.move_to(float(points[0][0]), float(points[0][1])) self.context.move_to(float(points[0][0]), float(points[0][1]))

View file

@ -1,8 +1,8 @@
""" """
Drawing separate map elements. Drawing separate map elements.
""" """
import argparse
import logging import logging
import sys
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
@ -13,15 +13,18 @@ from roentgen.point import Point
from roentgen.scheme import LineStyle, Scheme from roentgen.scheme import LineStyle, Scheme
from roentgen.workspace import workspace from roentgen.workspace import workspace
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
def draw_element(options) -> None:
def draw_element(options: argparse.Namespace) -> None:
"""Draw single node, line, or area.""" """Draw single node, line, or area."""
if options.node: if options.node:
target: str = "node" target: str = "node"
tags_description = options.node tags_description = options.node
else: else:
# Not implemented yet. # Not implemented yet.
sys.exit(1) exit(1)
tags: dict[str, str] = dict( tags: dict[str, str] = dict(
[x.split("=") for x in tags_description.split(",")] [x.split("=") for x in tags_description.split(",")]

View file

@ -1,7 +1,7 @@
""" """
Figures displayed on the map. Figures displayed on the map.
""" """
from typing import Any, Dict, List, Optional from typing import Any, Optional
import numpy as np import numpy as np
from colour import Color from colour import Color
@ -25,15 +25,15 @@ class Figure(Tagged):
def __init__( def __init__(
self, self,
tags: Dict[str, str], tags: dict[str, str],
inners: List[List[OSMNode]], inners: list[list[OSMNode]],
outers: List[List[OSMNode]], outers: list[list[OSMNode]],
) -> None: ) -> None:
super().__init__() super().__init__()
self.tags: Dict[str, str] = tags self.tags: dict[str, str] = tags
self.inners: List[List[OSMNode]] = [] self.inners: list[list[OSMNode]] = []
self.outers: List[List[OSMNode]] = [] self.outers: list[list[OSMNode]] = []
for inner_nodes in inners: for inner_nodes in inners:
self.inners.append(make_clockwise(inner_nodes)) self.inners.append(make_clockwise(inner_nodes))
@ -67,15 +67,15 @@ class Building(Figure):
def __init__( def __init__(
self, self,
tags: Dict[str, str], tags: dict[str, str],
inners: List[List[OSMNode]], inners: list[list[OSMNode]],
outers: List[List[OSMNode]], outers: list[list[OSMNode]],
flinger: Flinger, flinger: Flinger,
scheme: Scheme, scheme: Scheme,
) -> None: ) -> None:
super().__init__(tags, inners, outers) super().__init__(tags, inners, outers)
style: Dict[str, Any] = { style: dict[str, Any] = {
"fill": scheme.get_color("building_color").hex, "fill": scheme.get_color("building_color").hex,
"stroke": scheme.get_color("building_border_color").hex, "stroke": scheme.get_color("building_border_color").hex,
} }
@ -195,9 +195,9 @@ class StyledFigure(Figure):
def __init__( def __init__(
self, self,
tags: Dict[str, str], tags: dict[str, str],
inners: List[List[OSMNode]], inners: list[list[OSMNode]],
outers: List[List[OSMNode]], outers: list[list[OSMNode]],
line_style: LineStyle, line_style: LineStyle,
) -> None: ) -> None:
super().__init__(tags, inners, outers) super().__init__(tags, inners, outers)
@ -211,16 +211,16 @@ class Road(Figure):
def __init__( def __init__(
self, self,
tags: Dict[str, str], tags: dict[str, str],
inners: List[List[OSMNode]], inners: list[list[OSMNode]],
outers: List[List[OSMNode]], outers: list[list[OSMNode]],
matcher: RoadMatcher, matcher: RoadMatcher,
) -> None: ) -> None:
super().__init__(tags, inners, outers) super().__init__(tags, inners, outers)
self.matcher: RoadMatcher = matcher self.matcher: RoadMatcher = matcher
self.width: Optional[float] = None self.width: Optional[float] = None
self.lanes: List[Lane] = [] self.lanes: list[Lane] = []
if "lanes" in tags: if "lanes" in tags:
try: try:
@ -359,7 +359,7 @@ class Segment:
) # fmt: skip ) # fmt: skip
def is_clockwise(polygon: List[OSMNode]) -> bool: def is_clockwise(polygon: list[OSMNode]) -> bool:
""" """
Return true if polygon nodes are in clockwise order. Return true if polygon nodes are in clockwise order.
@ -374,7 +374,7 @@ def is_clockwise(polygon: List[OSMNode]) -> bool:
return count >= 0 return count >= 0
def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: def make_clockwise(polygon: list[OSMNode]) -> list[OSMNode]:
""" """
Make polygon nodes clockwise. Make polygon nodes clockwise.
@ -383,7 +383,7 @@ def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
return polygon if is_clockwise(polygon) else list(reversed(polygon)) return polygon if is_clockwise(polygon) else list(reversed(polygon))
def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: def make_counter_clockwise(polygon: list[OSMNode]) -> list[OSMNode]:
""" """
Make polygon nodes counter-clockwise. Make polygon nodes counter-clockwise.
@ -392,7 +392,7 @@ def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
return polygon if not is_clockwise(polygon) else list(reversed(polygon)) return polygon if not is_clockwise(polygon) else list(reversed(polygon))
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str: def get_path(nodes: list[OSMNode], shift: np.array, flinger: Flinger) -> str:
"""Construct SVG path commands from nodes.""" """Construct SVG path commands from nodes."""
path: str = "" path: str = ""
prev_node: Optional[OSMNode] = None prev_node: Optional[OSMNode] = None

View file

@ -34,6 +34,7 @@ class IconCollection:
background_color: Color = Color("white"), background_color: Color = Color("white"),
color: Color = Color("black"), color: Color = Color("black"),
add_unused: bool = False, add_unused: bool = False,
add_all: bool = False,
) -> "IconCollection": ) -> "IconCollection":
""" """
Collect all possible icon combinations in grid. Collect all possible icon combinations in grid.
@ -44,6 +45,7 @@ class IconCollection:
:param color: icon color :param color: icon color
:param add_unused: create icons from shapes that have no corresponding :param add_unused: create icons from shapes that have no corresponding
tags tags
:param add_all: create icons from all possible shapes including parts
""" """
icons: list[Icon] = [] icons: list[Icon] = []
@ -111,6 +113,13 @@ class IconCollection:
icon.recolor(color) icon.recolor(color)
icons.append(icon) icons.append(icon)
if add_all:
for shape_id in extractor.shapes.keys():
shape: Shape = extractor.get_shape(shape_id)
icon: Icon = Icon([ShapeSpecification(shape)])
icon.recolor(color)
icons.append(icon)
return cls(icons) return cls(icons)
def draw_icons( def draw_icons(
@ -204,8 +213,9 @@ def draw_icons() -> None:
extractor: ShapeExtractor = ShapeExtractor( extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
collection: IconCollection = IconCollection.from_scheme(scheme, extractor) collection: IconCollection = IconCollection.from_scheme(
scheme, extractor, add_all=True
)
icon_grid_path: Path = workspace.get_icon_grid_path() icon_grid_path: Path = workspace.get_icon_grid_path()
collection.draw_grid(icon_grid_path) collection.draw_grid(icon_grid_path)
logging.info(f"Icon grid is written to {icon_grid_path}.") logging.info(f"Icon grid is written to {icon_grid_path}.")

View file

@ -322,7 +322,7 @@ class ShapeSpecification:
def draw( def draw(
self, self,
svg, svg: Drawing,
point: np.array, point: np.array,
tags: dict[str, Any] = None, tags: dict[str, Any] = None,
outline: bool = False, outline: bool = False,
@ -372,7 +372,7 @@ class ShapeSpecification:
and np.allclose(self.offset, other.offset) and np.allclose(self.offset, other.offset)
) )
def __lt__(self, other) -> bool: def __lt__(self, other: "ShapeSpecification") -> bool:
return self.shape.id_ < other.shape.id_ return self.shape.id_ < other.shape.id_
@ -471,12 +471,12 @@ class Icon:
"""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: "Icon") -> bool:
return sorted(self.shape_specifications) == sorted( return sorted(self.shape_specifications) == sorted(
other.shape_specifications other.shape_specifications
) )
def __lt__(self, other) -> bool: def __lt__(self, other: "Icon") -> bool:
return "".join( return "".join(
[x.shape.id_ for x in self.shape_specifications] [x.shape.id_ for x in self.shape_specifications]
) < "".join([x.shape.id_ for x in other.shape_specifications]) ) < "".join([x.shape.id_ for x in other.shape_specifications])

View file

@ -1,6 +1,7 @@
""" """
MapCSS scheme creation. MapCSS scheme creation.
""" """
import argparse
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, TextIO from typing import Optional, TextIO
@ -175,7 +176,7 @@ class MapCSSWriter:
) )
def ui(options) -> None: def ui(options: argparse.Namespace) -> None:
"""Write MapCSS 0.2 scheme.""" """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,6 +1,7 @@
""" """
Simple OpenStreetMap renderer. Simple OpenStreetMap renderer.
""" """
import argparse
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, Iterator from typing import Any, Iterator
@ -17,12 +18,13 @@ 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.icon import ShapeExtractor
from roentgen.map_configuration import LabelMode, MapConfiguration
from roentgen.osm_getter import NetworkError, get_osm 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, BuildingMode, TIME_MODE, progress_bar from roentgen.ui import BuildingMode, progress_bar
from roentgen.workspace import workspace from roentgen.workspace import workspace
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -35,15 +37,19 @@ class Map:
""" """
def __init__( def __init__(
self, flinger: Flinger, svg: svgwrite.Drawing, scheme: Scheme, options self,
flinger: Flinger,
svg: svgwrite.Drawing,
scheme: Scheme,
configuration: MapConfiguration,
) -> None: ) -> None:
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.configuration = configuration
self.background_color: Color = self.scheme.get_color("background_color") self.background_color: Color = self.scheme.get_color("background_color")
if self.options.mode in [AUTHOR_MODE, TIME_MODE]: if self.configuration.is_wireframe():
self.background_color: Color = Color("#111111") self.background_color: Color = Color("#111111")
def draw(self, constructor: Constructor) -> None: def draw(self, constructor: Constructor) -> None:
@ -80,11 +86,13 @@ class Map:
# All other points # All other points
if self.options.overlap == 0: if self.configuration.overlap == 0:
occupied = None occupied = None
else: else:
occupied = Occupied( occupied = Occupied(
self.flinger.size[0], self.flinger.size[1], self.options.overlap self.flinger.size[0],
self.flinger.size[1],
self.configuration.overlap,
) )
nodes = sorted(constructor.points, key=lambda x: -x.priority) nodes = sorted(constructor.points, key=lambda x: -x.priority)
@ -105,18 +113,18 @@ class Map:
steps * 2 + index, steps * 3, step=10, text="Drawing texts" steps * 2 + index, steps * 3, step=10, text="Drawing texts"
) )
if ( if (
self.options.mode not in [TIME_MODE, AUTHOR_MODE] not self.configuration.is_wireframe()
and self.options.label_mode != "no" and self.configuration.label_mode != LabelMode.NO
): ):
point.draw_texts(self.svg, occupied, self.options.label_mode) point.draw_texts(
self.svg, occupied, self.configuration.label_mode
)
progress_bar(-1, len(nodes), step=10, text="Drawing nodes") 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_mode: BuildingMode = BuildingMode(self.options.buildings) if self.configuration.building_mode == BuildingMode.FLAT:
if building_mode == BuildingMode.FLAT:
for building in constructor.buildings: for building in constructor.buildings:
building.draw(self.svg, self.flinger) building.draw(self.svg, self.flinger)
return return
@ -197,12 +205,14 @@ class Map:
intersection.draw(self.svg, True) intersection.draw(self.svg, True)
def ui(options) -> None: def ui(options: argparse.Namespace) -> None:
""" """
Röntgen entry point. Röntgen entry point.
:param options: command-line arguments :param options: command-line arguments
""" """
configuration: MapConfiguration = MapConfiguration.from_options(options)
if not options.boundary_box and not options.input_file_name: if not options.boundary_box and not options.input_file_name:
logging.fatal("Specify either --boundary-box, or --input.") logging.fatal("Specify either --boundary-box, or --input.")
exit(1) exit(1)
@ -241,8 +251,7 @@ def ui(options) -> None:
osm_data = reader.osm_data osm_data = reader.osm_data
view_box = boundary_box view_box = boundary_box
else: else:
is_full: bool = options.mode in [AUTHOR_MODE, TIME_MODE] osm_reader = OSMReader(is_full=configuration.is_wireframe())
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():
@ -273,11 +282,13 @@ def ui(options) -> None:
flinger=flinger, flinger=flinger,
scheme=scheme, scheme=scheme,
icon_extractor=icon_extractor, icon_extractor=icon_extractor,
options=options, configuration=configuration,
) )
constructor.construct() constructor.construct()
painter: Map = Map(flinger=flinger, svg=svg, scheme=scheme, options=options) painter: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
)
painter.draw(constructor) painter.draw(constructor)
logging.info(f"Writing output SVG to {options.output_file_name}...") logging.info(f"Writing output SVG to {options.output_file_name}...")

View file

@ -8,6 +8,7 @@ import svgwrite
from colour import Color from colour import Color
from roentgen.icon import Icon, IconSet from roentgen.icon import Icon, IconSet
from roentgen.map_configuration import LabelMode
from roentgen.osm_reader import Tagged from roentgen.osm_reader import Tagged
from roentgen.text import Label from roentgen.text import Label
@ -161,14 +162,14 @@ class Point(Tagged):
self, self,
svg: svgwrite.Drawing, svg: svgwrite.Drawing,
occupied: Optional[Occupied] = None, occupied: Optional[Occupied] = None,
label_mode: str = "main", label_mode: LabelMode = LabelMode.MAIN,
) -> None: ) -> None:
"""Draw all labels.""" """Draw all labels."""
labels: list[Label] labels: list[Label]
if label_mode == "main": if label_mode == LabelMode.MAIN:
labels = self.labels[:1] labels = self.labels[:1]
elif label_mode == "all": elif label_mode == LabelMode.ALL:
labels = self.labels labels = self.labels
else: else:
return return

View file

@ -1,6 +1,7 @@
""" """
Röntgen tile server for slippy maps. Röntgen tile server for slippy maps.
""" """
import argparse
import logging import logging
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path from pathlib import Path
@ -62,7 +63,7 @@ class _Handler(SimpleHTTPRequestHandler):
return return
def ui(options) -> None: def ui(options: argparse.Namespace) -> None:
"""Command-line interface for tile server.""" """Command-line interface for tile server."""
server: Optional[HTTPServer] = None server: Optional[HTTPServer] = None
try: try:

View file

@ -3,6 +3,7 @@ Tile generation.
See https://wiki.openstreetmap.org/wiki/Tiles See https://wiki.openstreetmap.org/wiki/Tiles
""" """
import argparse
import logging import logging
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
@ -18,6 +19,7 @@ from roentgen.constructor import Constructor
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor from roentgen.icon import ShapeExtractor
from roentgen.mapper import Map from roentgen.mapper import Map
from roentgen.map_configuration import MapConfiguration
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
@ -115,23 +117,31 @@ 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, options) -> None: def draw(
self,
directory_name: Path,
cache_path: Path,
configuration: MapConfiguration,
) -> 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 :param configuration: 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, options) self.draw_with_osm_data(osm_data, directory_name, configuration)
def draw_with_osm_data( def draw_with_osm_data(
self, osm_data: OSMData, directory_name: Path, options self,
osm_data: OSMData,
directory_name: Path,
configuration: MapConfiguration,
) -> None: ) -> None:
"""Draw SVG and PNG tile using OpenStreetMap data.""" """Draw SVG and PNG tile using OpenStreetMap data."""
top, left = self.get_coordinates() top, left = self.get_coordinates()
@ -154,12 +164,12 @@ 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, options osm_data, flinger, scheme, icon_extractor, configuration
) )
constructor.construct() constructor.construct()
painter: Map = Map( painter: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, options=options flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
) )
painter.draw(constructor) painter.draw(constructor)
@ -216,7 +226,7 @@ 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( def draw_separately(
self, directory: Path, cache_path: Path, options self, directory: Path, cache_path: Path, configuration: MapConfiguration
) -> None: ) -> 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
@ -224,7 +234,7 @@ class Tiles:
: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 :param configuration: 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"
@ -235,7 +245,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, options) tile.draw_with_osm_data(osm_data, directory, configuration)
else: else:
logging.debug(f"File {file_path} already exists.") logging.debug(f"File {file_path} already exists.")
@ -253,19 +263,21 @@ 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, options) -> None: def draw(
self, directory: Path, cache_path: Path, configuration: MapConfiguration
) -> None:
""" """
Draw one PNG image with all tiles and split it into a set of separate 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 :param configuration: drawing configuration
""" """
if self.tiles_exist(directory): if self.tiles_exist(directory):
return return
self.draw_image(cache_path, options) self.draw_image(cache_path, configuration)
input_path: Path = self.get_file_path(cache_path).with_suffix(".png") 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:
@ -290,12 +302,14 @@ 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, options) -> None: def draw_image(
self, cache_path: Path, configuration: MapConfiguration
) -> 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 :param configuration: drawing configuration
""" """
output_path: Path = self.get_file_path(cache_path) output_path: Path = self.get_file_path(cache_path)
@ -319,16 +333,14 @@ class Tiles:
) )
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, options=options osm_data, flinger, scheme, extractor, configuration
) )
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( map_: Map = Map(flinger, svg, scheme, configuration)
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}...")
@ -346,9 +358,10 @@ class Tiles:
logging.debug(f"File {png_path} already exists.") logging.debug(f"File {png_path} already exists.")
def ui(options) -> None: def ui(options: argparse.Namespace) -> None:
"""Simple user interface for tile generation.""" """Simple user interface for tile generation."""
directory: Path = workspace.get_tile_path() directory: Path = workspace.get_tile_path()
configuration: MapConfiguration = MapConfiguration.from_options(options)
if options.coordinates: if options.coordinates:
coordinates: list[float] = list( coordinates: list[float] = list(
@ -356,13 +369,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), options) tile.draw(directory, Path(options.cache), configuration)
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), options) tile.draw(directory, Path(options.cache), configuration)
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
@ -370,7 +383,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), options) tiles.draw(directory, Path(options.cache), configuration)
else: else:
logging.fatal( logging.fatal(
"Specify either --coordinates, --boundary-box, or --tile." "Specify either --coordinates, --boundary-box, or --tile."

View file

@ -4,29 +4,15 @@ Command-line user interface.
import argparse import argparse
import sys import sys
from roentgen.map_configuration import BuildingMode, DrawingMode, LabelMode
from roentgen.osm_reader import STAGES_OF_DECAY
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
from enum import Enum
from roentgen.osm_reader import STAGES_OF_DECAY
BOXES: str = " ▏▎▍▌▋▊▉" BOXES: str = " ▏▎▍▌▋▊▉"
BOXES_LENGTH: int = len(BOXES) BOXES_LENGTH: int = len(BOXES)
AUTHOR_MODE: str = "author"
TIME_MODE: str = "time"
class BuildingMode(Enum):
"""
Building drawing mode.
"""
FLAT = "flat"
ISOMETRIC = "isometric"
ISOMETRIC_NO_PARTS = "isometric-no-parts"
def parse_options(args) -> argparse.Namespace: def parse_options(args) -> argparse.Namespace:
"""Parse Röntgen command-line options.""" """Parse Röntgen command-line options."""
@ -68,8 +54,9 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--mode", "--mode",
default="normal", default="normal",
help="map drawing mode",
metavar="<string>", metavar="<string>",
choices=(x.value for x in DrawingMode),
help="map drawing mode: " + ", ".join(x.value for x in DrawingMode),
) )
parser.add_argument( parser.add_argument(
"--overlap", "--overlap",
@ -81,10 +68,11 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None:
) )
parser.add_argument( parser.add_argument(
"--labels", "--labels",
help="label drawing mode: `no`, `main`, or `all`",
dest="label_mode", dest="label_mode",
default="main", default="main",
metavar="<string>", metavar="<string>",
choices=(x.value for x in LabelMode),
help="label drawing mode: " + ", ".join(x.value for x in LabelMode),
) )
parser.add_argument( parser.add_argument(
"-s", "-s",
@ -99,6 +87,12 @@ def add_map_arguments(parser: argparse.ArgumentParser) -> None:
default="overground", default="overground",
help="display only this floor level", help="display only this floor level",
) )
parser.add_argument(
"--seed",
default="",
help="seed for random",
metavar="<string>",
)
def add_tile_arguments(parser: argparse.ArgumentParser) -> None: def add_tile_arguments(parser: argparse.ArgumentParser) -> None:
@ -179,12 +173,6 @@ def add_render_arguments(parser: argparse.ArgumentParser) -> None:
default="cache", default="cache",
metavar="<path>", metavar="<path>",
) )
parser.add_argument(
"--seed",
default="",
help="seed for random",
metavar="<string>",
)
def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None: def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
@ -225,14 +213,13 @@ def progress_bar(
:param text: short description :param text: short description
""" """
if number == -1: if number == -1:
print(f"100 % {length * ''}{text}") sys.stdout.write(f"100 % {length * ''}{text}\n")
elif number % step == 0: elif number % step == 0:
ratio: float = number / total ratio: float = number / total
parts: int = int(ratio * length * BOXES_LENGTH) parts: int = int(ratio * length * BOXES_LENGTH)
fill_length: int = int(parts / BOXES_LENGTH) fill_length: int = int(parts / BOXES_LENGTH)
box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)] box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)]
print( sys.stdout.write(
f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * ''}{box}" f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * ''}{box}"
f"{int(length - fill_length - 1) * ' '}{text}" f"{int(length - fill_length - 1) * ' '}{text}\n\033[F"
) )
sys.stdout.write("\033[F")