mirror of
https://github.com/enzet/map-machine.git
synced 2025-06-01 10:21:54 +02:00
Issue #91: support automatic icon generation.
Use regular expressions to match tags and construct icons using matched substrings.
This commit is contained in:
parent
4644d38166
commit
39acfc6ff6
4 changed files with 102 additions and 88 deletions
|
@ -50,12 +50,13 @@ class IconCollection:
|
|||
"""
|
||||
icons: list[Icon] = []
|
||||
|
||||
def add() -> Icon:
|
||||
def add() -> Optional[Icon]:
|
||||
"""Construct icon and add it to the list."""
|
||||
specifications: list[ShapeSpecification] = [
|
||||
scheme.get_shape_specification(x, extractor)
|
||||
for x in current_set
|
||||
]
|
||||
specifications: list[ShapeSpecification] = []
|
||||
for shape_specification in current_set:
|
||||
if "#" in shape_specification["shape"]:
|
||||
return None
|
||||
scheme.get_shape_specification(shape_specification, extractor)
|
||||
constructed_icon: Icon = Icon(specifications)
|
||||
constructed_icon.recolor(color, white=background_color)
|
||||
if constructed_icon not in icons:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Map Machine drawing scheme.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
@ -49,13 +50,14 @@ class MatchingType(Enum):
|
|||
MATCHED_BY_SET = 1
|
||||
MATCHED_BY_WILDCARD = 2
|
||||
MATCHED = 3
|
||||
MATCHED_BY_REGEX = 4
|
||||
|
||||
|
||||
def is_matched_tag(
|
||||
matcher_tag_key: str,
|
||||
matcher_tag_value: Union[str, list],
|
||||
tags: dict[str, str],
|
||||
) -> MatchingType:
|
||||
) -> tuple[MatchingType, list[str]]:
|
||||
"""
|
||||
Check whether element tags contradict tag matcher.
|
||||
|
||||
|
@ -63,20 +65,20 @@ def is_matched_tag(
|
|||
:param matcher_tag_value: tag value, tag value list, or "*"
|
||||
:param tags: element tags to check
|
||||
"""
|
||||
if matcher_tag_key in tags:
|
||||
if matcher_tag_value == "*":
|
||||
return MatchingType.MATCHED_BY_WILDCARD
|
||||
if (
|
||||
isinstance(matcher_tag_value, str)
|
||||
and tags[matcher_tag_key] == matcher_tag_value
|
||||
):
|
||||
return MatchingType.MATCHED
|
||||
if (
|
||||
isinstance(matcher_tag_value, list)
|
||||
and tags[matcher_tag_key] in matcher_tag_value
|
||||
):
|
||||
return MatchingType.MATCHED_BY_SET
|
||||
return MatchingType.NOT_MATCHED
|
||||
if matcher_tag_key not in tags:
|
||||
return MatchingType.NOT_MATCHED, []
|
||||
|
||||
if matcher_tag_value == "*":
|
||||
return MatchingType.MATCHED_BY_WILDCARD, []
|
||||
if tags[matcher_tag_key] == matcher_tag_value:
|
||||
return MatchingType.MATCHED, []
|
||||
matcher: Optional[re.Match] = re.match(
|
||||
matcher_tag_value, tags[matcher_tag_key]
|
||||
)
|
||||
if matcher:
|
||||
return MatchingType.MATCHED_BY_REGEX, list(matcher.groups())
|
||||
|
||||
return MatchingType.NOT_MATCHED, []
|
||||
|
||||
|
||||
def get_selector(key: str, value: str, prefix: str = "") -> str:
|
||||
|
@ -139,13 +141,15 @@ class Matcher:
|
|||
self,
|
||||
tags: dict[str, str],
|
||||
configuration: Optional[MapConfiguration] = None,
|
||||
) -> bool:
|
||||
) -> 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
|
||||
"""
|
||||
groups: dict[str, str] = {}
|
||||
|
||||
if (
|
||||
configuration is not None
|
||||
and self.location_restrictions
|
||||
|
@ -153,28 +157,29 @@ class Matcher:
|
|||
self.location_restrictions, configuration.country
|
||||
)
|
||||
):
|
||||
return False
|
||||
return False, {}
|
||||
|
||||
for config_tag_key in self.tags:
|
||||
config_tag_key: str
|
||||
tag_matcher = self.tags[config_tag_key]
|
||||
if (
|
||||
is_matched_tag(config_tag_key, tag_matcher, tags)
|
||||
== MatchingType.NOT_MATCHED
|
||||
):
|
||||
return False
|
||||
is_matched, matched_groups = is_matched_tag(
|
||||
config_tag_key, self.tags[config_tag_key], tags
|
||||
)
|
||||
if is_matched == MatchingType.NOT_MATCHED:
|
||||
return False, {}
|
||||
elif matched_groups:
|
||||
for index, element in enumerate(matched_groups):
|
||||
groups[f"#{config_tag_key}{index}"] = element
|
||||
|
||||
if self.exception:
|
||||
for config_tag_key in self.exception:
|
||||
config_tag_key: str
|
||||
tag_matcher = self.exception[config_tag_key]
|
||||
if (
|
||||
is_matched_tag(config_tag_key, tag_matcher, tags)
|
||||
!= MatchingType.NOT_MATCHED
|
||||
):
|
||||
return False
|
||||
is_matched, matched_groups = is_matched_tag(
|
||||
config_tag_key, self.exception[config_tag_key], tags
|
||||
)
|
||||
if is_matched != MatchingType.NOT_MATCHED:
|
||||
return False, {}
|
||||
|
||||
return True
|
||||
return True, groups
|
||||
|
||||
def get_mapcss_selector(self, prefix: str = "") -> str:
|
||||
"""
|
||||
|
@ -195,6 +200,19 @@ class Matcher:
|
|||
return {}
|
||||
|
||||
|
||||
def get_shape_specifications(
|
||||
structure: list[Union[str, dict[str, Any]]]
|
||||
) -> list[dict]:
|
||||
"""Parse shape specification from scheme."""
|
||||
shapes: list[dict] = []
|
||||
for shape_specification in structure:
|
||||
if isinstance(shape_specification, str):
|
||||
shapes.append({"shape": shape_specification})
|
||||
else:
|
||||
shapes.append(shape_specification)
|
||||
return shapes
|
||||
|
||||
|
||||
class NodeMatcher(Matcher):
|
||||
"""
|
||||
Tag specification matcher.
|
||||
|
@ -212,15 +230,15 @@ class NodeMatcher(Matcher):
|
|||
|
||||
self.shapes: Optional[IconDescription] = None
|
||||
if "shapes" in structure:
|
||||
self.shapes = structure["shapes"]
|
||||
self.shapes = get_shape_specifications(structure["shapes"])
|
||||
|
||||
self.over_icon: Optional[IconDescription] = None
|
||||
if "over_icon" in structure:
|
||||
self.over_icon = structure["over_icon"]
|
||||
self.over_icon = get_shape_specifications(structure["over_icon"])
|
||||
|
||||
self.add_shapes: Optional[IconDescription] = None
|
||||
if "add_shapes" in structure:
|
||||
self.add_shapes = structure["add_shapes"]
|
||||
self.add_shapes = get_shape_specifications(structure["add_shapes"])
|
||||
|
||||
self.set_main_color: Optional[str] = None
|
||||
if "set_main_color" in structure:
|
||||
|
@ -232,17 +250,17 @@ class NodeMatcher(Matcher):
|
|||
|
||||
self.under_icon: Optional[IconDescription] = None
|
||||
if "under_icon" in structure:
|
||||
self.under_icon = structure["under_icon"]
|
||||
self.under_icon = get_shape_specifications(structure["under_icon"])
|
||||
|
||||
self.with_icon: Optional[IconDescription] = None
|
||||
if "with_icon" in structure:
|
||||
self.with_icon = structure["with_icon"]
|
||||
self.with_icon = get_shape_specifications(structure["with_icon"])
|
||||
|
||||
def get_clean_shapes(self) -> Optional[list[str]]:
|
||||
"""Get list of shape identifiers for shapes."""
|
||||
if not self.shapes:
|
||||
return None
|
||||
return [(x if isinstance(x, str) else x["shape"]) for x in self.shapes]
|
||||
return [x["shape"] for x in self.shapes]
|
||||
|
||||
|
||||
class WayMatcher(Matcher):
|
||||
|
@ -288,6 +306,7 @@ class RoadMatcher(Matcher):
|
|||
self.priority = structure["priority"]
|
||||
|
||||
def get_priority(self, tags: dict[str, str]) -> float:
|
||||
"""Get priority for drawing order."""
|
||||
layer: float = 0
|
||||
if "layer" in tags:
|
||||
layer = float(tags.get("layer"))
|
||||
|
@ -349,6 +368,7 @@ class Scheme:
|
|||
try:
|
||||
return Color(color)
|
||||
except (ValueError, AttributeError):
|
||||
logging.debug(f"Unknown color `{color}`.")
|
||||
return DEFAULT_COLOR
|
||||
|
||||
def is_no_drawable(self, key: str) -> bool:
|
||||
|
@ -410,7 +430,8 @@ class Scheme:
|
|||
for index, matcher in enumerate(self.node_matchers):
|
||||
if not matcher.replace_shapes and main_icon:
|
||||
continue
|
||||
if not matcher.is_matched(tags, configuration):
|
||||
matching, groups = matcher.is_matched(tags, configuration)
|
||||
if not matching:
|
||||
continue
|
||||
if (
|
||||
not configuration.ignore_level_matching
|
||||
|
@ -423,7 +444,7 @@ class Scheme:
|
|||
processed |= matcher_tags
|
||||
if matcher.shapes:
|
||||
specifications = [
|
||||
self.get_shape_specification(x, extractor)
|
||||
self.get_shape_specification(x, extractor, groups)
|
||||
for x in matcher.shapes
|
||||
]
|
||||
main_icon = Icon(specifications)
|
||||
|
@ -437,7 +458,9 @@ class Scheme:
|
|||
processed |= matcher_tags
|
||||
if matcher.add_shapes:
|
||||
specifications = [
|
||||
self.get_shape_specification(x, extractor, Color("#888888"))
|
||||
self.get_shape_specification(
|
||||
x, extractor, color=Color("#888888")
|
||||
)
|
||||
for x in matcher.add_shapes
|
||||
]
|
||||
extra_icons += [Icon(specifications)]
|
||||
|
@ -493,7 +516,8 @@ class Scheme:
|
|||
line_styles = []
|
||||
|
||||
for matcher in self.way_matchers:
|
||||
if not matcher.is_matched(tags):
|
||||
matching, _ = matcher.is_matched(tags)
|
||||
if not matching:
|
||||
continue
|
||||
|
||||
line_styles.append(LineStyle(matcher.style, matcher.priority))
|
||||
|
@ -503,7 +527,8 @@ class Scheme:
|
|||
def get_road(self, tags: dict[str, Any]) -> Optional[RoadMatcher]:
|
||||
"""Get road matcher if tags are matched."""
|
||||
for matcher in self.road_matchers:
|
||||
if not matcher.is_matched(tags):
|
||||
matching, _ = matcher.is_matched(tags)
|
||||
if not matching:
|
||||
continue
|
||||
return matcher
|
||||
return None
|
||||
|
@ -586,7 +611,8 @@ class Scheme:
|
|||
def is_area(self, tags: dict[str, str]) -> bool:
|
||||
"""Check whether way described by tags is area."""
|
||||
for matcher in self.area_matchers:
|
||||
if matcher.is_matched(tags):
|
||||
matching, _ = matcher.is_matched(tags)
|
||||
if matching:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -605,6 +631,7 @@ class Scheme:
|
|||
self,
|
||||
structure: Union[str, dict[str, Any]],
|
||||
extractor: ShapeExtractor,
|
||||
groups: dict[str, str] = None,
|
||||
color: Color = DEFAULT_COLOR,
|
||||
) -> ShapeSpecification:
|
||||
"""
|
||||
|
@ -619,25 +646,25 @@ class Scheme:
|
|||
flip_vertically: bool = False
|
||||
use_outline: bool = True
|
||||
|
||||
if isinstance(structure, str):
|
||||
shape = extractor.get_shape(structure)
|
||||
elif isinstance(structure, dict):
|
||||
if "shape" in structure:
|
||||
shape = extractor.get_shape(structure["shape"])
|
||||
else:
|
||||
logging.error(
|
||||
"Invalid shape specification: `shape` key expected."
|
||||
)
|
||||
if "color" in structure:
|
||||
color = self.get_color(structure["color"])
|
||||
if "offset" in structure:
|
||||
offset = np.array(structure["offset"])
|
||||
if "flip_horizontally" in structure:
|
||||
flip_horizontally = structure["flip_horizontally"]
|
||||
if "flip_vertically" in structure:
|
||||
flip_vertically = structure["flip_vertically"]
|
||||
if "outline" in structure:
|
||||
use_outline = structure["outline"]
|
||||
structure: dict[str, Any]
|
||||
if "shape" in structure:
|
||||
shape_id: str = structure["shape"]
|
||||
if groups:
|
||||
for key in groups:
|
||||
shape_id = shape_id.replace(key, groups[key])
|
||||
shape = extractor.get_shape(shape_id)
|
||||
else:
|
||||
logging.error("Invalid shape specification: `shape` key expected.")
|
||||
if "color" in structure:
|
||||
color = self.get_color(structure["color"])
|
||||
if "offset" in structure:
|
||||
offset = np.array(structure["offset"])
|
||||
if "flip_horizontally" in structure:
|
||||
flip_horizontally = structure["flip_horizontally"]
|
||||
if "flip_vertically" in structure:
|
||||
flip_vertically = structure["flip_vertically"]
|
||||
if "outline" in structure:
|
||||
use_outline = structure["outline"]
|
||||
|
||||
return ShapeSpecification(
|
||||
shape,
|
||||
|
|
|
@ -1267,29 +1267,11 @@ node_icons:
|
|||
shapes: [buffer_stop]
|
||||
- tags: {traffic_sign: city_limit}
|
||||
shapes: [city_limit_sign]
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "30"}
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "(\\d)(\\d)"}
|
||||
shapes: [
|
||||
circle_11,
|
||||
{shape: digit_3, offset: [-2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_0, offset: [2, 0], color: "#FFFFFF"},
|
||||
]
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "40"}
|
||||
shapes: [
|
||||
circle_11,
|
||||
{shape: digit_4, offset: [-2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_0, offset: [2, 0], color: "#FFFFFF"},
|
||||
]
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "50"}
|
||||
shapes: [
|
||||
circle_11,
|
||||
{shape: digit_5, offset: [-2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_0, offset: [2, 0], color: "#FFFFFF"},
|
||||
]
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "60"}
|
||||
shapes: [
|
||||
circle_11,
|
||||
{shape: digit_6, offset: [-2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_0, offset: [2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_#maxspeed0, offset: [-2, 0], color: "#FFFFFF"},
|
||||
{shape: digit_#maxspeed1, offset: [2, 0], color: "#FFFFFF"},
|
||||
]
|
||||
- tags: {traffic_sign: maxspeed, maxspeed: "40_mph"}
|
||||
shapes: [
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Test icon generation for nodes.
|
||||
"""
|
||||
import pytest
|
||||
from colour import Color
|
||||
|
||||
from map_machine.grid import IconCollection
|
||||
from map_machine.icon import IconSet
|
||||
|
@ -53,8 +54,11 @@ def test_icon() -> None:
|
|||
Tags that should be visualized with single main icon and without extra
|
||||
icons.
|
||||
"""
|
||||
icon = get_icon({"natural": "tree"})
|
||||
icon: IconSet = get_icon({"natural": "tree"})
|
||||
assert not icon.main_icon.is_default()
|
||||
assert len(icon.main_icon.shape_specifications) == 1
|
||||
assert icon.main_icon.shape_specifications[0].shape.id_ == "tree"
|
||||
assert icon.main_icon.shape_specifications[0].color == Color("#98AC64")
|
||||
assert not icon.extra_icons
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue