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