mirror of
https://github.com/enzet/map-machine.git
synced 2025-08-06 10:09:52 +02:00
Issue #75: add filter for icons by zoom level.
This commit is contained in:
parent
9c4873c5ae
commit
39428cad19
13 changed files with 147 additions and 62 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue