Issue #140: put scheme inside map configuration.

Refactoring of `Scheme` and `MapConfiguration` classes.
This commit is contained in:
Sergey Vartanov 2022-08-15 10:46:25 +03:00
parent 596910fe01
commit c76e1f0301
12 changed files with 114 additions and 70 deletions

View file

@ -156,13 +156,12 @@ class Constructor:
self,
osm_data: OSMData,
flinger: Flinger,
scheme: Scheme,
extractor: ShapeExtractor,
configuration: MapConfiguration,
) -> None:
self.osm_data: OSMData = osm_data
self.flinger: Flinger = flinger
self.scheme: Scheme = scheme
self.scheme: Scheme = configuration.scheme
self.extractor: ShapeExtractor = extractor
self.configuration: MapConfiguration = configuration
self.text_constructor: TextConstructor = TextConstructor(self.scheme)
@ -306,8 +305,8 @@ class Constructor:
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed, self.configuration
icon_set, priority = self.configuration.get_icon(
self.extractor, line.tags, processed
)
if icon_set is not None:
labels: list[Label] = self.text_constructor.construct_text(
@ -347,8 +346,8 @@ class Constructor:
processed: set[str] = set()
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed, self.configuration
icon_set, priority = self.configuration.get_icon(
self.extractor, line.tags, processed
)
if icon_set is not None:
labels: list[Label] = self.text_constructor.construct_text(
@ -413,6 +412,8 @@ class Constructor:
"""Draw nodes."""
logging.info("Constructing nodes...")
# Sort node vertically (using latitude values) to draw them from top to
# bottom.
sorted_node_ids: Iterator[int] = sorted(
self.osm_data.nodes.keys(),
key=lambda x: -self.osm_data.nodes[x].coordinates[0],
@ -423,6 +424,7 @@ class Constructor:
def construct_node(self, node: OSMNode) -> None:
"""Draw one node."""
tags: dict[str, str] = node.tags
if not tags:
return
if not self.check_level(tags):
@ -472,8 +474,8 @@ class Constructor:
color = Color("#CCCCCC")
if self.configuration.drawing_mode == DrawingMode.BLACK:
color = Color("#444444")
icon_set, priority = self.scheme.get_icon(
self.extractor, tags, processed, self.configuration
icon_set, priority = self.configuration.get_icon(
self.extractor, tags, processed
)
icon_set.main_icon.recolor(color)
point: Point = Point(
@ -487,8 +489,8 @@ class Constructor:
self.points.append(point)
return
icon_set, priority = self.scheme.get_icon(
self.extractor, tags, processed, self.configuration
icon_set, priority = self.configuration.get_icon(
self.extractor, tags, processed
)
if icon_set is None:
return

View file

@ -142,8 +142,8 @@ class SVGTable:
if column_value:
current_tags |= {self.collection.column_key: column_value}
processed: set[str] = set()
icon, _ = SCHEME.get_icon(
EXTRACTOR, current_tags, processed, MapConfiguration()
icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, current_tags, processed
)
processed = icon.processed
if not icon:

View file

@ -38,23 +38,26 @@ def draw(
input_file_name: Path,
output_file_name: Path,
boundary_box: BoundaryBox,
configuration: MapConfiguration = MapConfiguration(),
configuration: Optional[MapConfiguration] = None,
) -> None:
"""Draw file."""
if configuration is None:
configuration = MapConfiguration(SCHEME)
osm_data: OSMData = OSMData()
osm_data.parse_osm_file(input_file_name)
flinger: Flinger = Flinger(
boundary_box, configuration.zoom_level, osm_data.equator_length
)
constructor: Constructor = Constructor(
osm_data, flinger, SCHEME, EXTRACTOR, configuration
osm_data, flinger, EXTRACTOR, configuration
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_file_name), size=flinger.size
)
map_: Map = Map(flinger, svg, SCHEME, configuration)
map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor)
svg.write(output_file_name.open("w"))
@ -63,11 +66,14 @@ def draw(
def draw_around_point(
point: np.ndarray,
name: str,
configuration: MapConfiguration = MapConfiguration(),
configuration: Optional[MapConfiguration] = None,
size: np.ndarray = np.array((600, 400)),
get: Optional[BoundaryBox] = None,
) -> None:
"""Draw around point."""
if configuration is None:
configuration = MapConfiguration(SCHEME)
input_path: Path = doc_path / f"{name}.svg"
boundary_box: BoundaryBox = BoundaryBox.from_coordinates(
@ -90,7 +96,7 @@ def main(id_: str) -> None:
draw_around_point(
np.array((55.75277, 37.40856)),
"fitness",
MapConfiguration(zoom_level=20.2),
MapConfiguration(SCHEME, zoom_level=20.2),
np.array((300, 200)),
)
@ -98,14 +104,14 @@ def main(id_: str) -> None:
draw_around_point(
np.array((52.5622, 12.94)),
"power",
configuration=MapConfiguration(zoom_level=15),
configuration=MapConfiguration(SCHEME, zoom_level=15),
)
if id_ is None or id_ == "playground":
draw_around_point(
np.array((52.47388, 13.43826)),
"playground",
configuration=MapConfiguration(zoom_level=19),
configuration=MapConfiguration(SCHEME, zoom_level=19),
)
# Playground: (59.91991/10.85535), (59.83627/10.83017), Oslo
@ -116,6 +122,7 @@ def main(id_: str) -> None:
np.array((52.50892, 13.3244)),
"surveillance",
MapConfiguration(
SCHEME,
zoom_level=18.5,
ignore_level_matching=True,
),
@ -126,6 +133,7 @@ def main(id_: str) -> None:
np.array((52.421, 13.101)),
"viewpoints",
MapConfiguration(
SCHEME,
label_mode=LabelMode.NO,
zoom_level=15.7,
ignore_level_matching=True,
@ -136,7 +144,7 @@ def main(id_: str) -> None:
draw_around_point(
np.array((-26.19049, 28.05605)),
"buildings",
MapConfiguration(building_mode=BuildingMode.ISOMETRIC),
MapConfiguration(SCHEME, building_mode=BuildingMode.ISOMETRIC),
)
if id_ is None or id_ == "trees":
@ -144,7 +152,7 @@ def main(id_: str) -> None:
np.array((55.751, 37.628)),
"trees",
MapConfiguration(
label_mode=LabelMode(LabelMode.ALL), zoom_level=18.1
SCHEME, label_mode=LabelMode(LabelMode.ALL), zoom_level=18.1
),
get=BoundaryBox(37.624, 55.749, 37.633, 55.753),
)
@ -158,6 +166,7 @@ def main(id_: str) -> None:
np.array((55.7655, 37.6055)),
"time",
MapConfiguration(
SCHEME,
DrawingMode.TIME,
zoom_level=16.5,
ignore_level_matching=True,
@ -169,6 +178,7 @@ def main(id_: str) -> None:
np.array((55.7655, 37.6055)),
"author",
MapConfiguration(
SCHEME,
DrawingMode.AUTHOR,
seed="a",
zoom_level=16.5,
@ -181,6 +191,7 @@ def main(id_: str) -> None:
np.array((48.87422, 2.377)),
"colors",
configuration=MapConfiguration(
SCHEME,
zoom_level=17.6,
building_mode=BuildingMode.ISOMETRIC,
ignore_level_matching=True,

View file

@ -58,11 +58,10 @@ class WikiTable:
text += f"{{{{Tag|{key}|{value}}}}}<br />"
text = text[:-6]
text += "\n"
icon, _ = SCHEME.get_icon(
EXTRACTOR,
current_tags | self.collection.tags,
set(),
MapConfiguration(ignore_level_matching=True),
icon, _ = MapConfiguration(
SCHEME, ignore_level_matching=True
).get_icon(
EXTRACTOR, current_tags | self.collection.tags, set()
)
icons.append(icon.main_icon)
text += (
@ -103,7 +102,9 @@ class WikiTable:
}
if column_value:
current_tags |= {self.collection.column_key: column_value}
icon, _ = SCHEME.get_icon(EXTRACTOR, current_tags, set())
icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, current_tags, set()
)
if not icon:
print("Icon was not constructed.")
text += (
@ -135,8 +136,8 @@ def generate_new_text(
wiki_text, icons = table.generate_wiki_table()
else:
processed = set()
icon, _ = SCHEME.get_icon(
EXTRACTOR, table.collection.tags, processed, MapConfiguration()
icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, table.collection.tags, processed
)
if not icon.main_icon.is_default():
wiki_text = (

View file

@ -94,17 +94,17 @@ class Grid:
def draw(self, output_path: Path, zoom: float = DEFAULT_ZOOM) -> None:
"""Draw grid."""
configuration: MapConfiguration = MapConfiguration(
level="all", credit=None
SCHEME, level="all", credit=None
)
flinger: Flinger = Flinger(
self.get_boundary_box(), zoom, self.osm_data.equator_length
)
svg: Drawing = Drawing(output_path.name, flinger.size)
constructor: Constructor = Constructor(
self.osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, configuration
self.osm_data, flinger, SHAPE_EXTRACTOR, configuration
)
constructor.construct()
map_: Map = Map(flinger, svg, SCHEME, configuration)
map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor)
for text, i, j in self.texts:

View file

@ -8,7 +8,7 @@ import svgwrite
from svgwrite.path import Path as SVGPath
from map_machine.element.grid import Grid
from map_machine.map_configuration import LabelMode
from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm.osm_reader import Tags
from map_machine.pictogram.icon import ShapeExtractor
from map_machine.pictogram.point import Point
@ -49,7 +49,7 @@ def draw_element(options: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
processed: set[str] = set()
icon, _ = scheme.get_icon(extractor, tags, processed)
icon, _ = MapConfiguration(scheme).get_icon(extractor, tags, processed)
is_for_node: bool = target == "node"
text_constructor: TextConstructor = TextConstructor(scheme)
labels: list[Label] = text_constructor.construct_text(

View file

@ -2,10 +2,13 @@
import argparse
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from typing import Optional, Any
from colour import Color
from map_machine.pictogram.icon import ShapeExtractor, IconSet
from map_machine.scheme import Scheme
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
@ -42,6 +45,7 @@ class BuildingMode(Enum):
class MapConfiguration:
"""Map drawing configuration."""
scheme: Scheme
drawing_mode: DrawingMode = DrawingMode.NORMAL
building_mode: BuildingMode = BuildingMode.FLAT
label_mode: LabelMode = LabelMode.MAIN
@ -59,10 +63,11 @@ class MapConfiguration:
@classmethod
def from_options(
cls, options: argparse.Namespace, zoom_level: float
cls, scheme: Scheme, options: argparse.Namespace, zoom_level: float
) -> "MapConfiguration":
"""Initialize from command-line options."""
return cls(
scheme,
DrawingMode(options.mode),
BuildingMode(options.buildings),
LabelMode(options.label_mode),
@ -87,3 +92,19 @@ class MapConfiguration:
if self.drawing_mode not in (DrawingMode.NORMAL, DrawingMode.BLACK):
return Color("#111111")
return None
def get_icon(
self,
extractor: ShapeExtractor,
tags: dict[str, Any],
processed: set[str],
) -> tuple[Optional[IconSet], int]:
return self.scheme.get_icon(
extractor,
tags,
processed,
self.country,
self.zoom_level,
self.ignore_level_matching,
self.show_overlapped,
)

View file

@ -44,12 +44,11 @@ class Map:
self,
flinger: Flinger,
svg: svgwrite.Drawing,
scheme: Scheme,
configuration: MapConfiguration,
) -> None:
self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg
self.scheme: Scheme = scheme
self.scheme: Scheme = configuration.scheme
self.configuration = configuration
self.background_color: Color = self.scheme.get_color("background_color")
@ -263,8 +262,12 @@ def render_map(arguments: argparse.Namespace) -> None:
:param arguments: command-line arguments
"""
scheme: Scheme = Scheme.from_file(
workspace.find_scheme_path(arguments.scheme)
)
configuration: MapConfiguration = MapConfiguration.from_options(
arguments, float(arguments.zoom)
scheme, arguments, float(arguments.zoom)
)
cache_path: Path = Path(arguments.cache)
cache_path.mkdir(parents=True, exist_ok=True)
@ -347,19 +350,15 @@ def render_map(arguments: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data=osm_data,
flinger=flinger,
scheme=scheme,
extractor=icon_extractor,
configuration=configuration,
)
constructor.construct()
map_: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
)
map_: Map = Map(flinger=flinger, svg=svg, configuration=configuration)
map_.draw(constructor)
logging.info(f"Writing output SVG to {arguments.output_file_name}...")

View file

@ -11,7 +11,6 @@ import yaml
from colour import Color
from map_machine.feature.direction import DirectionSet
from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import Tagged, Tags
from map_machine.pictogram.icon import (
DEFAULT_SHAPE_ID,
@ -134,22 +133,21 @@ class Matcher(Tagged):
)
def is_matched(
self, tags: Tags, configuration: Optional[MapConfiguration] = None
self, tags: Tags, country: Optional[str] = None
) -> tuple[bool, dict[str, str]]:
"""
Check whether element tags matches tag matcher.
:param tags: element tags to be matched
:param configuration: current map configuration to be matched
:param country: country of the element (to match location restrictions
if any)
"""
groups: dict[str, str] = {}
if (
configuration is not None
country is not None
and self.location_restrictions
and not match_location(
self.location_restrictions, configuration.country
)
and not match_location(self.location_restrictions, country)
):
return False, {}
@ -476,7 +474,10 @@ class Scheme:
extractor: ShapeExtractor,
tags: dict[str, Any],
processed: set[str],
configuration: MapConfiguration = MapConfiguration(),
country: Optional[str] = None,
zoom_level: float = 18,
ignore_level_matching: bool = False,
show_overlapped: bool = False,
) -> tuple[Optional[IconSet], int]:
"""
Construct icon set.
@ -484,7 +485,11 @@ class Scheme:
:param extractor: extractor with icon specifications
:param tags: OpenStreetMap element tags dictionary
:param processed: set of already processed tag keys
:param configuration: current map configuration to be matched
:param country: country to match location restrictions
:param zoom_level: current map zoom level
:param ignore_level_matching: do not check level for the icon
:param show_overlapped: get small dot instead of icon if point is
overlapped by some other points
:return (icon set, icon priority)
"""
tags_hash: str = (
@ -501,12 +506,11 @@ class Scheme:
for index, matcher in enumerate(self.node_matchers):
if not matcher.replace_shapes and main_icon:
continue
matching, groups = matcher.is_matched(tags, configuration)
matching, groups = matcher.is_matched(tags, country)
if not matching:
continue
if (
not configuration.ignore_level_matching
and not matcher.check_zoom_level(configuration.zoom_level)
if not ignore_level_matching and not matcher.check_zoom_level(
zoom_level
):
return None, 0
matcher_tags: set[str] = set(matcher.tags.keys())
@ -567,7 +571,7 @@ class Scheme:
main_icon.recolor(color)
default_icon: Optional[Icon] = None
if configuration.show_overlapped:
if show_overlapped:
small_dot_spec: ShapeSpecification = ShapeSpecification(
extractor.get_shape(DEFAULT_SMALL_SHAPE_ID),
color if color else self.get_color("default"),

View file

@ -173,14 +173,13 @@ class Tile:
icon_extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data, flinger, scheme, icon_extractor, configuration
osm_data, flinger, icon_extractor, configuration
)
constructor.construct()
painter: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
flinger=flinger, svg=svg, configuration=configuration
)
painter.draw(constructor)
@ -390,16 +389,15 @@ class Tiles:
extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data, flinger, scheme, extractor, configuration
osm_data, flinger, extractor, configuration
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size
)
map_: Map = Map(flinger, svg, scheme, configuration)
map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...")
@ -472,6 +470,10 @@ def generate_tiles(options: argparse.Namespace) -> None:
zoom_levels: list[int] = parse_zoom_level(options.zoom)
min_zoom_level: int = min(zoom_levels)
scheme: Scheme = Scheme.from_file(
workspace.find_scheme_path(options.scheme)
)
if options.input_file_name:
osm_data: OSMData = OSMData()
osm_data.parse_osm_file(Path(options.input_file_name))
@ -487,7 +489,7 @@ def generate_tiles(options: argparse.Namespace) -> None:
for zoom_level in zoom_levels:
configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level
scheme, options, zoom_level
)
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
tiles.draw(directory, Path(options.cache), configuration, osm_data)
@ -510,7 +512,7 @@ def generate_tiles(options: argparse.Namespace) -> None:
)
try:
configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level
scheme, options, zoom_level
)
tile.draw_with_osm_data(osm_data, directory, configuration)
except NetworkError as error:
@ -520,7 +522,7 @@ def generate_tiles(options: argparse.Namespace) -> None:
zoom_level, x, y = map(int, options.tile.split("/"))
tile: Tile = Tile(x, y, zoom_level)
configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level
scheme, options, zoom_level
)
tile.draw(directory, Path(options.cache), configuration)
@ -544,7 +546,7 @@ def generate_tiles(options: argparse.Namespace) -> None:
else:
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level
scheme, options, zoom_level
)
tiles.draw(directory, Path(options.cache), configuration, osm_data)

View file

@ -9,6 +9,7 @@ from typing import Optional
from colour import Color
from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import Tags
from map_machine.pictogram.icon import IconSet, ShapeSpecification, Icon
from map_machine.pictogram.icon_collection import IconCollection
@ -18,6 +19,7 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
COLLECTION: IconCollection = IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR)
DEFAULT_COLOR: Color = SCHEME.get_default_color()
EXTRA_COLOR: Color = SCHEME.get_extra_color()
@ -48,7 +50,7 @@ def test_icons_by_name() -> None:
def get_icon(tags: Tags) -> IconSet:
"""Construct icon from tags."""
processed: set[str] = set()
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed)
icon, _ = CONFIGURATION.get_icon(SHAPE_EXTRACTOR, tags, processed)
return icon

View file

@ -14,6 +14,8 @@ from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags
from tests import SCHEME, SHAPE_EXTRACTOR
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
def get_constructor(osm_data: OSMData) -> Constructor:
"""
@ -24,7 +26,7 @@ def get_constructor(osm_data: OSMData) -> Constructor:
BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length
)
constructor: Constructor = Constructor(
osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, MapConfiguration()
osm_data, flinger, SHAPE_EXTRACTOR, CONFIGURATION
)
constructor.construct_ways()
return constructor