mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-02 19:56:39 +02:00
If tags has "direction" key, direction is mostly to the left (to the West), and shape for the icon is right directed, we flip the shape horizontally.
400 lines
13 KiB
Python
400 lines
13 KiB
Python
"""
|
|
Röntgen drawing scheme.
|
|
|
|
Author: Sergey Vartanov (me@enzet.ru).
|
|
"""
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
|
|
import yaml
|
|
from colour import Color
|
|
|
|
from roentgen.direction import DirectionSet
|
|
from roentgen.icon import (
|
|
DEFAULT_COLOR, DEFAULT_SHAPE_ID, Icon, IconSet, ShapeExtractor,
|
|
ShapeSpecification
|
|
)
|
|
from roentgen.text import Label, get_address, get_text
|
|
|
|
|
|
@dataclass
|
|
class LineStyle:
|
|
"""
|
|
SVG line style and its priority.
|
|
"""
|
|
style: Dict[str, Union[int, float, str]]
|
|
priority: float = 0.0
|
|
|
|
|
|
class MatchingType(Enum):
|
|
"""
|
|
Description on how tag was matched.
|
|
"""
|
|
NOT_MATCHED = 0
|
|
MATCHED_BY_SET = 1
|
|
MATCHED_BY_WILDCARD = 2
|
|
MATCHED = 3
|
|
|
|
|
|
def is_matched_tag(
|
|
matcher_tag_key: str, matcher_tag_value: Union[str, list],
|
|
tags: Dict[str, str]
|
|
) -> MatchingType:
|
|
"""
|
|
Check whether element tags contradict tag matcher.
|
|
|
|
:param matcher_tag_key: tag key
|
|
: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
|
|
|
|
|
|
def is_matched(matcher: Dict[str, Any], tags: Dict[str, str]) -> bool:
|
|
"""
|
|
Check whether element tags matches tag matcher.
|
|
|
|
:param matcher: dictionary with tag keys and values, value lists, or "*"
|
|
:param tags: element tags to match
|
|
"""
|
|
matched: bool = True
|
|
|
|
for config_tag_key in matcher["tags"]:
|
|
config_tag_key: str
|
|
tag_matcher = matcher["tags"][config_tag_key]
|
|
if (
|
|
is_matched_tag(config_tag_key, tag_matcher, tags) ==
|
|
MatchingType.NOT_MATCHED
|
|
):
|
|
matched = False
|
|
break
|
|
|
|
if "no_tags" in matcher:
|
|
for config_tag_key in matcher["no_tags"]:
|
|
config_tag_key: str
|
|
tag_matcher = matcher["no_tags"][config_tag_key]
|
|
if (
|
|
is_matched_tag(config_tag_key, tag_matcher, tags) !=
|
|
MatchingType.NOT_MATCHED
|
|
):
|
|
matched = False
|
|
break
|
|
|
|
return matched
|
|
|
|
|
|
class Scheme:
|
|
"""
|
|
Map style.
|
|
|
|
Specifies map colors and rules to draw icons for OpenStreetMap tags.
|
|
"""
|
|
def __init__(self, file_name: str):
|
|
"""
|
|
:param file_name: scheme file name with tags, colors, and tag key
|
|
specification
|
|
"""
|
|
with open(file_name) as input_file:
|
|
content: Dict[str, Any] = yaml.load(
|
|
input_file.read(), Loader=yaml.FullLoader)
|
|
|
|
self.icons: List[Dict[str, Any]] = content["node_icons"]
|
|
self.ways: List[Dict[str, Any]] = content["ways"]
|
|
|
|
self.colors: Dict[str, str] = content["colors"]
|
|
|
|
self.area_tags: List[Dict[str, str]] = content["area_tags"]
|
|
|
|
self.tags_to_write: List[str] = content["tags_to_write"]
|
|
self.prefix_to_write: List[str] = content["prefix_to_write"]
|
|
self.tags_to_skip: List[str] = content["tags_to_skip"]
|
|
self.prefix_to_skip: List[str] = content["prefix_to_skip"]
|
|
|
|
# Storage for created icon sets.
|
|
self.cache: Dict[str, Tuple[IconSet, int]] = {}
|
|
|
|
def get_color(self, color: str) -> Color:
|
|
"""
|
|
Return color if the color is in scheme, otherwise return default color.
|
|
|
|
:param color: input color string representation
|
|
:return: 6-digit color specification with "#"
|
|
"""
|
|
if color in self.colors:
|
|
return Color(self.colors[color])
|
|
if color.lower() in self.colors:
|
|
return Color(self.colors[color.lower()])
|
|
try:
|
|
return Color(color)
|
|
except ValueError:
|
|
return DEFAULT_COLOR
|
|
|
|
def is_no_drawable(self, key: str) -> bool:
|
|
"""
|
|
Return true if key is specified as no drawable (should not be
|
|
represented on the map as icon set or as text) by the scheme.
|
|
|
|
:param key: OpenStreetMap tag key
|
|
"""
|
|
if key in self.tags_to_write or key in self.tags_to_skip:
|
|
return True
|
|
for prefix in self.prefix_to_write + self.prefix_to_skip: # type: str
|
|
if key[:len(prefix) + 1] == f"{prefix}:":
|
|
return True
|
|
return False
|
|
|
|
def is_writable(self, key: str) -> bool:
|
|
"""
|
|
Return true if key is specified as writable (should be represented on
|
|
the map as text) by the scheme.
|
|
|
|
:param key: OpenStreetMap tag key
|
|
"""
|
|
if key in self.tags_to_skip:
|
|
return False
|
|
if key in self.tags_to_write:
|
|
return True
|
|
for prefix in self.prefix_to_write: # type: str
|
|
if key[:len(prefix) + 1] == f"{prefix}:":
|
|
return True
|
|
return False
|
|
|
|
def get_icon(
|
|
self,
|
|
icon_extractor: ShapeExtractor,
|
|
tags: Dict[str, Any],
|
|
for_: str = "node",
|
|
) -> Tuple[IconSet, int]:
|
|
"""
|
|
Construct icon set.
|
|
|
|
:param icon_extractor: extractor with icon specifications
|
|
:param tags: OpenStreetMap element tags dictionary
|
|
:param for_: target (node, way, area or relation)
|
|
:return (icon set, icon priority)
|
|
"""
|
|
tags_hash: str = (
|
|
",".join(tags.keys()) + ":" + ",".join(map(str, tags.values()))
|
|
)
|
|
if tags_hash in self.cache:
|
|
return self.cache[tags_hash]
|
|
|
|
main_icon: Optional[Icon] = None
|
|
extra_icons: List[Icon] = []
|
|
processed: Set[str] = set()
|
|
priority: int = 0
|
|
|
|
for index, matcher in enumerate(self.icons):
|
|
index: int
|
|
matcher: Dict[str, Any]
|
|
matched: bool = is_matched(matcher, tags)
|
|
matcher_tags: Set[str] = matcher["tags"].keys()
|
|
if not matched:
|
|
continue
|
|
priority = len(self.icons) - index
|
|
if "draw" in matcher and not matcher["draw"]:
|
|
processed |= set(matcher_tags)
|
|
if "icon" in matcher:
|
|
main_icon = Icon([
|
|
ShapeSpecification.from_structure(x, icon_extractor, self)
|
|
for x in matcher["icon"]
|
|
])
|
|
processed |= set(matcher_tags)
|
|
if "over_icon" in matcher:
|
|
if main_icon:
|
|
main_icon.add_specifications([
|
|
ShapeSpecification.from_structure(
|
|
x, icon_extractor, self
|
|
)
|
|
for x in matcher["over_icon"]
|
|
])
|
|
for key in matcher_tags:
|
|
processed.add(key)
|
|
if "add_icon" in matcher:
|
|
extra_icons += [Icon([
|
|
ShapeSpecification.from_structure(
|
|
x, icon_extractor, self, Color("#888888")
|
|
)
|
|
for x in matcher["add_icon"]
|
|
])]
|
|
for key in matcher_tags:
|
|
processed.add(key)
|
|
if "color" in matcher:
|
|
assert False
|
|
if "set_main_color" in matcher:
|
|
main_icon.recolor(self.get_color(matcher["set_main_color"]))
|
|
|
|
color: Optional[Color] = None
|
|
|
|
for tag_key in tags: # type: str
|
|
if (tag_key.endswith(":color") or
|
|
tag_key.endswith(":colour")):
|
|
color = self.get_color(tags[tag_key])
|
|
processed.add(tag_key)
|
|
|
|
for tag_key in tags: # type: str
|
|
if tag_key in ["color", "colour"]:
|
|
color = self.get_color(tags[tag_key])
|
|
processed.add(tag_key)
|
|
|
|
if main_icon and color:
|
|
main_icon.recolor(color)
|
|
|
|
keys_left = [
|
|
x for x in tags.keys()
|
|
if x not in processed and not self.is_no_drawable(x)
|
|
]
|
|
|
|
default_shape = icon_extractor.get_shape(DEFAULT_SHAPE_ID)
|
|
if not main_icon:
|
|
main_icon = Icon([ShapeSpecification(default_shape)])
|
|
|
|
returned: IconSet = IconSet(main_icon, extra_icons, processed)
|
|
self.cache[tags_hash] = returned, priority
|
|
|
|
if "direction" in tags:
|
|
if DirectionSet(tags["direction"]).is_right() is False:
|
|
for specification in main_icon.shape_specifications:
|
|
if specification.shape.is_right_directed:
|
|
specification.flip_horizontally = True
|
|
|
|
return returned, priority
|
|
|
|
def get_style(self, tags: Dict[str, Any], scale):
|
|
"""
|
|
Get line style based on tags and scale.
|
|
"""
|
|
line_styles = []
|
|
|
|
for element in self.ways: # type: Dict[str, Any]
|
|
priority = 0
|
|
matched: bool = is_matched(element, tags)
|
|
if not matched:
|
|
continue
|
|
style: Dict[str, Any] = {"fill": "none"}
|
|
if "priority" in element:
|
|
priority = element["priority"]
|
|
for key in element: # type: str
|
|
if key not in [
|
|
"tags", "no_tags", "priority", "level", "icon", "r", "r1",
|
|
"r2"
|
|
]:
|
|
value = element[key]
|
|
if isinstance(value, str) and value.endswith("_color"):
|
|
value = self.get_color(value)
|
|
style[key] = value
|
|
if "r" in element:
|
|
style["stroke-width"] = (element["r"] * scale)
|
|
if "r1" in element:
|
|
style["stroke-width"] = (element["r1"] * scale + 1)
|
|
if "r2" in element:
|
|
style["stroke-width"] = (element["r2"] * scale + 2)
|
|
|
|
line_styles.append(LineStyle(style, priority))
|
|
|
|
return line_styles
|
|
|
|
def construct_text(
|
|
self, tags: Dict[str, str], draw_captions: str
|
|
) -> List[Label]:
|
|
"""
|
|
Construct labels for not processed tags.
|
|
"""
|
|
texts: List[Label] = []
|
|
|
|
name = None
|
|
alt_name = None
|
|
if "name" in tags:
|
|
name = tags["name"]
|
|
tags.pop("name", None)
|
|
if "name:ru" in tags:
|
|
if not name:
|
|
name = tags["name:ru"]
|
|
tags.pop("name:ru", None)
|
|
tags.pop("name:ru", None)
|
|
if "name:en" in tags:
|
|
if not name:
|
|
name = tags["name:en"]
|
|
tags.pop("name:en", None)
|
|
tags.pop("name:en", None)
|
|
if "alt_name" in tags:
|
|
if alt_name:
|
|
alt_name += ", "
|
|
else:
|
|
alt_name = ""
|
|
alt_name += tags["alt_name"]
|
|
tags.pop("alt_name")
|
|
if "old_name" in tags:
|
|
if alt_name:
|
|
alt_name += ", "
|
|
else:
|
|
alt_name = ""
|
|
alt_name += "ex " + tags["old_name"]
|
|
|
|
address: List[str] = get_address(tags, draw_captions)
|
|
|
|
if name:
|
|
texts.append(Label(name, Color("black")))
|
|
if alt_name:
|
|
texts.append(Label(f"({alt_name})"))
|
|
if address:
|
|
texts.append(Label(", ".join(address)))
|
|
|
|
if draw_captions == "main":
|
|
return texts
|
|
|
|
texts += get_text(tags)
|
|
|
|
if "route_ref" in tags:
|
|
texts.append(Label(tags["route_ref"].replace(";", " ")))
|
|
tags.pop("route_ref", None)
|
|
if "cladr:code" in tags:
|
|
texts.append(Label(tags["cladr:code"], size=7))
|
|
tags.pop("cladr:code", None)
|
|
if "website" in tags:
|
|
link = tags["website"]
|
|
if link[:7] == "http://":
|
|
link = link[7:]
|
|
if link[:8] == "https://":
|
|
link = link[8:]
|
|
if link[:4] == "www.":
|
|
link = link[4:]
|
|
if link[-1] == "/":
|
|
link = link[:-1]
|
|
link = link[:25] + ("..." if len(tags["website"]) > 25 else "")
|
|
texts.append(Label(link, Color("#000088")))
|
|
tags.pop("website", None)
|
|
for key in ["phone"]:
|
|
if key in tags:
|
|
texts.append(Label(tags[key], Color("#444444")))
|
|
tags.pop(key)
|
|
if "height" in tags:
|
|
texts.append(Label(f"↕ {tags['height']} m"))
|
|
tags.pop("height")
|
|
for tag in tags:
|
|
if self.is_writable(tag):
|
|
texts.append(Label(tags[tag]))
|
|
return texts
|
|
|
|
def is_area(self, tags: Dict[str, str]) -> bool:
|
|
"""
|
|
Check whether way described by tags is area.
|
|
"""
|
|
for matcher in self.area_tags:
|
|
if is_matched(matcher, tags):
|
|
return True
|
|
return False
|