Issue #75: add filter for icons by zoom level.

This commit is contained in:
Sergey Vartanov 2021-08-29 01:50:59 +03:00
parent 9c4873c5ae
commit 39428cad19
13 changed files with 147 additions and 62 deletions

View file

@ -251,8 +251,12 @@ class Constructor:
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed
self.extractor,
line.tags,
processed,
self.configuration.zoom_level,
)
if icon_set is not None:
labels: list[Label] = self.scheme.construct_text(
line.tags, "all", processed
)
@ -264,7 +268,7 @@ class Constructor:
center_point,
is_for_node=False,
priority=priority,
) # fmt: skip
)
self.points.append(point)
if not line_styles:
@ -284,15 +288,24 @@ class Constructor:
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed
self.extractor,
line.tags,
processed,
self.configuration.zoom_level,
)
if icon_set is not None:
labels: list[Label] = self.scheme.construct_text(
line.tags, "all", processed
)
point: Point = Point(
icon_set, labels, line.tags, processed, center_point,
is_for_node=False, priority=priority,
) # fmt: skip
icon_set,
labels,
line.tags,
processed,
center_point,
is_for_node=False,
priority=priority,
)
self.points.append(point)
def draw_special_mode(
@ -385,8 +398,10 @@ class Constructor:
return
icon_set, priority = self.scheme.get_icon(
self.extractor, tags, processed
self.extractor, tags, processed, self.configuration.zoom_level
)
if icon_set is None:
return
labels: list[Label] = self.scheme.construct_text(tags, "all", processed)
self.scheme.process_ignored(tags, processed)

View file

@ -8,8 +8,8 @@ from typing import Optional, Union
import cairo
import numpy as np
import svgwrite
from colour import Color
from cairo import Context, ImageSurface
from colour import Color
from svgwrite.base import BaseElement
from svgwrite.path import Path as SVGPath
from svgwrite.shapes import Rect

View file

@ -41,7 +41,7 @@ def draw_element(options: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
processed: set[str] = set()
icon, priority = scheme.get_icon(extractor, tags, processed)
icon, priority = scheme.get_icon(extractor, tags, processed, 18)
is_for_node: bool = target == "node"
labels: list[Label] = scheme.construct_text(tags, "all", processed)
point: Point = Point(

View file

@ -19,6 +19,9 @@ from roentgen.scheme import LineStyle, RoadMatcher, Scheme
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
BUILDING_HEIGHT_SCALE: float = 2.5
BUILDING_MINIMAL_HEIGHT: float = 8.0
class Figure(Tagged):
"""
@ -34,13 +37,10 @@ class Figure(Tagged):
super().__init__()
self.tags: dict[str, str] = tags
self.inners: list[list[OSMNode]] = []
self.outers: list[list[OSMNode]] = []
for inner_nodes in inners:
self.inners.append(make_clockwise(inner_nodes))
for outer_nodes in outers:
self.outers.append(make_counter_clockwise(outer_nodes))
self.inners: list[list[OSMNode]] = list(map(make_clockwise, inners))
self.outers: list[list[OSMNode]] = list(
map(make_counter_clockwise, outers)
)
def get_path(
self, flinger: Flinger, shift: np.ndarray = np.array((0, 0))
@ -92,16 +92,16 @@ class Building(Figure):
self.parts = sorted(self.parts)
self.height: float = 8.0
self.height: float = BUILDING_MINIMAL_HEIGHT
self.min_height: float = 0.0
levels: Optional[str] = self.get_float("building:levels")
if levels:
self.height = float(levels) * 2.5
self.height = float(levels) * BUILDING_HEIGHT_SCALE
levels: Optional[str] = self.get_float("building:min_level")
if levels:
self.min_height = float(levels) * 2.5
self.min_height = float(levels) * BUILDING_HEIGHT_SCALE
height: Optional[float] = self.get_length("height")
if height:

View file

@ -48,6 +48,7 @@ class MapConfiguration:
drawing_mode: DrawingMode = DrawingMode.NORMAL
building_mode: BuildingMode = BuildingMode.FLAT
label_mode: LabelMode = LabelMode.MAIN
zoom_level: int = 18
overlap: int = 12
level: str = "overground"
seed: str = ""
@ -59,6 +60,7 @@ class MapConfiguration:
DrawingMode(options.mode),
BuildingMode(options.buildings),
LabelMode(options.label_mode),
options.zoom,
options.overlap,
options.level,
options.seed,

View file

@ -94,13 +94,19 @@ class Matcher:
Tag matching.
"""
def __init__(self, structure: dict[str, Any]) -> None:
def __init__(
self, structure: dict[str, Any], group: Optional[dict[str, Any]] = None
) -> None:
self.tags: dict[str, str] = structure["tags"]
self.exception: dict[str, str] = {}
if "exception" in structure:
self.exception = structure["exception"]
self.start_zoom_level: Optional[int] = None
if group is not None and "start_zoom_level" in group:
self.start_zoom_level = group["start_zoom_level"]
self.replace_shapes: bool = True
if "replace_shapes" in structure:
self.replace_shapes = structure["replace_shapes"]
@ -109,6 +115,13 @@ class Matcher:
if "location_restrictions" in structure:
self.location_restrictions = structure["location_restrictions"]
def check_zoom_level(self, zoom_level: int):
"""Check whether zoom level is matching."""
return (
self.start_zoom_level is None
or zoom_level >= self.start_zoom_level
)
def is_matched(self, tags: dict[str, str]) -> bool:
"""
Check whether element tags matches tag matcher.
@ -118,8 +131,6 @@ class Matcher:
if self.location_restrictions:
return False # FIXME: implement
matched: bool = True
for config_tag_key in self.tags:
config_tag_key: str
tag_matcher = self.tags[config_tag_key]
@ -127,8 +138,7 @@ class Matcher:
is_matched_tag(config_tag_key, tag_matcher, tags)
== MatchingType.NOT_MATCHED
):
matched = False
break
return False
if self.exception:
for config_tag_key in self.exception:
@ -138,10 +148,9 @@ class Matcher:
is_matched_tag(config_tag_key, tag_matcher, tags)
!= MatchingType.NOT_MATCHED
):
matched = False
break
return False
return matched
return True
def get_mapcss_selector(self, prefix: str = "") -> str:
"""
@ -167,9 +176,11 @@ class NodeMatcher(Matcher):
Tag specification matcher.
"""
def __init__(self, structure: dict[str, Any]) -> None:
def __init__(
self, structure: dict[str, Any], group: dict[str, Any]
) -> None:
# Dictionary with tag keys and values, value lists, or "*"
super().__init__(structure)
super().__init__(structure, group)
self.draw: bool = True
if "draw" in structure:
@ -268,7 +279,7 @@ class Scheme:
self.node_matchers: list[NodeMatcher] = []
for group in content["node_icons"]:
for element in group["tags"]:
self.node_matchers.append(NodeMatcher(element))
self.node_matchers.append(NodeMatcher(element, group))
self.colors: dict[str, str] = content["colors"]
self.material_colors: dict[str, str] = content["material_colors"]
@ -341,13 +352,15 @@ class Scheme:
extractor: ShapeExtractor,
tags: dict[str, Any],
processed: set[str],
) -> tuple[IconSet, int]:
zoom_level: int,
) -> tuple[Optional[IconSet], int]:
"""
Construct icon set.
:param extractor: extractor with icon specifications
:param tags: OpenStreetMap element tags dictionary
:param processed: set of already processed tag keys
:param zoom_level: zoom level in current context
:return (icon set, icon priority)
"""
tags_hash: str = (
@ -365,9 +378,10 @@ class Scheme:
for matcher in self.node_matchers:
if not matcher.replace_shapes and main_icon:
continue
matched: bool = matcher.is_matched(tags)
if not matched:
if not matcher.is_matched(tags):
continue
if not matcher.check_zoom_level(zoom_level):
return None, 0
matcher_tags: set[str] = set(matcher.tags.keys())
priority = len(self.node_matchers) - index
if not matcher.draw:

View file

@ -42,14 +42,14 @@ class _Handler(SimpleHTTPRequestHandler):
zoom_level: int = int(parts[2])
x: int = int(parts[3])
y: int = int(parts[4])
tile: Tile = Tile(x, y, zoom_level)
tile_path: Path = workspace.get_tile_path()
png_path: Path = tile_path / f"tile_{zoom_level}_{x}_{y}.png"
svg_path: Path = tile.get_file_name(tile_path)
png_path: Path = svg_path.with_suffix(".png")
if self.update_cache:
svg_path: Path = png_path.with_suffix(".svg")
if not png_path.exists():
if not svg_path.exists():
tile = Tile(x, y, zoom_level)
tile.draw(tile_path, self.cache, self.options)
with svg_path.open() as input_file:
cairosvg.svg2png(

View file

@ -9,6 +9,7 @@ from colour import Color
__author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru"
DEFAULT_FONT_SIZE: float = 10.0
DEFAULT_COLOR: Color = Color("#444444")
@ -20,7 +21,7 @@ class Label:
text: str
fill: Color = DEFAULT_COLOR
size: float = 10.0
size: float = DEFAULT_FONT_SIZE
def get_address(

View file

@ -15,15 +15,15 @@ import numpy as np
import svgwrite
from PIL import Image
from roentgen.boundary_box import BoundaryBox
from roentgen.constructor import Constructor
from roentgen.flinger import Flinger
from roentgen.icon import ShapeExtractor
from roentgen.mapper import Map
from roentgen.map_configuration import MapConfiguration
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.boundary_box import BoundaryBox
from roentgen.workspace import workspace
__author__ = "Sergey Vartanov"
@ -398,8 +398,6 @@ def parse_zoom_level(zoom_level_specification: str) -> list[int]:
def parse(zoom_level: str) -> int:
"""Parse zoom level."""
parsed_zoom_level: int = int(zoom_level)
if parsed_zoom_level <= 0:
raise ScaleConfigurationException("Non positive zoom level.")
if parsed_zoom_level > 20:
raise ScaleConfigurationException("Scale is too big.")
return parsed_zoom_level

View file

@ -97,7 +97,8 @@ node_icons:
- tags: {building: "yes"}
draw: false
- group: "Transport hubs"
- group: "Huge transport hubs"
start_zoom_level: 10
tags:
- tags: {amenity: ferry_terminal}
shapes: [anchor]
@ -111,6 +112,10 @@ node_icons:
shapes: [h]
- tags: {aeroway: spaceport}
shapes: [rocket_on_launch_pad]
- group: "Normal transport hubs"
start_zoom_level: 11
tags:
- tags: {aeroway: launchpad}
shapes: [rocket_flying]
- tags: {aeroway: landingpad}
@ -157,6 +162,7 @@ node_icons:
shapes: [taxi]
- group: "Big territory"
start_zoom_level: 12
tags:
- tags: {leisure: fishing}
shapes: [fishing_angle]
@ -198,6 +204,7 @@ node_icons:
shapes: [{shape: pear, color: orchard_border_color}]
- group: "Bigger objects"
start_zoom_level: 13
tags:
- tags: {waterway: waterfall}
shapes: [{shape: waterfall, color: water_border_color}]
@ -281,6 +288,7 @@ node_icons:
shapes: [slide_and_water]
- group: "Important big objects"
start_zoom_level: 14
tags:
- tags: {amenity: pharmacy}
shapes: [medicine_bottle]
@ -349,6 +357,7 @@ node_icons:
location_restrictions: {include: jp}
- group: "Normal big objects"
start_zoom_level: 15
tags:
- tags: {shop: supermarket}
shapes: [supermarket_cart]
@ -529,6 +538,7 @@ node_icons:
shapes: [table]
- group: "Big objects not for all"
start_zoom_level: 15
tags:
- tags: {building: apartments}
shapes: [apartments]
@ -551,6 +561,7 @@ node_icons:
shapes: [telephone]
- group: "Not important big objects"
start_zoom_level: 15
tags:
- tags: {man_made: communications_tower}
location_restrictions: {include: jp}
@ -571,6 +582,7 @@ node_icons:
shapes: [garages]
- group: "Emergency"
start_zoom_level: 15
tags:
- tags: {emergency: defibrillator}
shapes: [{shape: defibrillator, color: emergency_color}]
@ -584,6 +596,7 @@ node_icons:
shapes: [{shape: sos_phone, color: emergency_color}]
- group: "Transport-important middle objects"
start_zoom_level: 16
tags:
- tags: {ford: "yes"}
shapes: [ford]
@ -625,6 +638,7 @@ node_icons:
shapes: [bump]
- group: "Important middle objects"
start_zoom_level: 16
tags:
- tags: {tourism: attraction, attraction: amusement_ride}
shapes: [amusement_ride]
@ -634,6 +648,7 @@ node_icons:
shapes: [shelter]
- group: "Normal middle objects"
start_zoom_level: 17
tags:
- tags: {shop: kiosk}
shapes: [kiosk]
@ -653,6 +668,7 @@ node_icons:
shapes: [pipeline]
- group: "Not important middle objects"
start_zoom_level: 17
tags:
- tags: {building: ventilation_shaft}
shapes: [ventilation]
@ -915,6 +931,7 @@ node_icons:
add_shapes: [phone]
- group: "Important small objects"
start_zoom_level: 17
tags:
- tags: {historic: cannon}
shapes: [cannon]
@ -999,6 +1016,7 @@ node_icons:
shapes: {christmas_tree}
- group: "Normal small objects"
start_zoom_level: 18
tags:
- tags: {amenity: binoculars}
shapes: [binoculars_on_pole]
@ -1044,6 +1062,7 @@ node_icons:
shapes: [golf_pin]
- group: "Entrances"
start_zoom_level: 18
tags:
- tags: {amenity: parking_entrance}
shapes:
@ -1073,6 +1092,7 @@ node_icons:
shapes: [no_door]
- group: "Not important small objects"
start_zoom_level: 18
tags:
- tags: {amenity: bench}
shapes: [bench]
@ -1228,6 +1248,7 @@ node_icons:
shapes: [bollard]
- group: "Indoor"
start_zoom_level: 18
tags:
- tags: {door: "yes"}
shapes: [entrance]

View file

@ -24,7 +24,7 @@ def test_grid(init_collection: IconCollection) -> None:
def test_icons_by_id(init_collection: IconCollection) -> None:
"""Test individual icons drawing."""
init_collection.draw_icons(workspace.get_icons_by_id_path(), by_name=False)
init_collection.draw_icons(workspace.get_icons_by_id_path())
def test_icons_by_name(init_collection: IconCollection) -> None:
@ -35,7 +35,7 @@ def test_icons_by_name(init_collection: IconCollection) -> None:
def get_icon(tags: dict[str, str]) -> IconSet:
"""Construct icon from tags."""
processed: set[str] = set()
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed)
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed, 18)
return icon

View file

@ -13,7 +13,7 @@ def test_mapcss() -> None:
"""Test MapCSS generation."""
writer: MapCSSWriter = MapCSSWriter(SCHEME, "icons")
matcher: NodeMatcher = NodeMatcher(
{"tags": {"natural": "tree"}, "shapes": ["tree"]}
{"tags": {"natural": "tree"}, "shapes": ["tree"]}, {}
)
selector = writer.add_selector("node", matcher)
assert (

View file

@ -1,23 +1,57 @@
"""
Test zoom level specification parsing.
"""
from roentgen.tile import parse_zoom_level
from roentgen.tile import ScaleConfigurationException, parse_zoom_level
def test_zoom_level_1() -> None:
"""Test one zoom level."""
assert parse_zoom_level("18") == [18]
def test_zoom_level_list() -> None:
"""Test list of zoom levels."""
assert parse_zoom_level("17,18") == [17, 18]
assert parse_zoom_level("16,17,18") == [16, 17, 18]
def test_zoom_level_range() -> None:
"""Test range of zoom levels."""
assert parse_zoom_level("16-18") == [16, 17, 18]
assert parse_zoom_level("18-18") == [18]
def test_zoom_level_mixed() -> None:
"""Test zoom level specification with list of numbers and ranges."""
assert parse_zoom_level("15,16-18") == [15, 16, 17, 18]
assert parse_zoom_level("15,16-18,20") == [15, 16, 17, 18, 20]
def test_zoom_level_too_big() -> None:
"""Test too big zoom level."""
try:
parse_zoom_level("21")
except ScaleConfigurationException:
return
assert False
def test_zoom_level_negative() -> None:
"""Test negative zoom level."""
try:
parse_zoom_level("-1")
except ValueError:
return
assert False
def test_zoom_level_wrong() -> None:
"""Test too big zoom level."""
try:
parse_zoom_level(",")
except ValueError:
return
assert False