From afaeb6f37ae057d60560b3a161ee4fa1e02e30c4 Mon Sep 17 00:00:00 2001 From: Sergey Vartanov Date: Tue, 27 Jul 2021 03:29:17 +0300 Subject: [PATCH] Issue #63: add ghost icons. Add keys with lifecycle prefixes to MapCSS with ghost icons (icons with low opacity). --- roentgen/mapcss.py | 99 +++++++++++++++++++++++++++++------------- roentgen/osm_reader.py | 12 +++++ roentgen/scheme.py | 23 +++++----- 3 files changed, 94 insertions(+), 40 deletions(-) diff --git a/roentgen/mapcss.py b/roentgen/mapcss.py index 7110972..c06bf49 100644 --- a/roentgen/mapcss.py +++ b/roentgen/mapcss.py @@ -2,39 +2,80 @@ MapCSS scheme creation. """ from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional import logging from colour import Color from roentgen.grid import IconCollection from roentgen.icon import ShapeExtractor -from roentgen.scheme import Scheme +from roentgen.osm_reader import STAGES_OF_DECAY +from roentgen.scheme import Scheme, Matcher -def construct_selectors(scheme: Scheme, icon_directory_name: str) -> str: - """ - Construct icon selectors for MapCSS 0.2 scheme. - """ - selectors: Dict[str, List[str]] = {} - for matcher in scheme.node_matchers: - if matcher.shapes and not matcher.location_restrictions: - # TODO: support location restrictions - selectors[matcher.get_mapcss_selector()] = [ - (x if isinstance(x, str) else x["shape"]) - for x in matcher.shapes - ] +class MapCSSWriter: + def __init__( + self, + scheme: Scheme, + icon_directory_name: str, + add_icons_for_lifecycle: bool, + ): + self.add_icons_for_lifecycle = add_icons_for_lifecycle + self.icon_directory_name = icon_directory_name - s: str = "" - for selector in selectors: - for target in ["node", "area"]: - s += ( - target + selector + " {\n" - f' icon-image: "{icon_directory_name}/' - + "___".join(selectors[selector]) - + '.svg";\n}\n' - ) - return s + self.matchers: Dict[Matcher, List[str]] = {} + for matcher in scheme.node_matchers: + if matcher.shapes and not matcher.location_restrictions: + self.matchers[matcher] = [ + (x if isinstance(x, str) else x["shape"]) + for x in matcher.shapes + ] + + def add_selector( + self, + target: str, + matcher: Matcher, + prefix: str = "", + opacity: Optional[float] = None, + ) -> str: + selector = ( + target + matcher.get_mapcss_selector(prefix) + " {\n" + f' icon-image: "{self.icon_directory_name}/' + + "___".join(self.matchers[matcher]) + + '.svg";\n' + ) + if opacity is not None: + selector += f" icon-opacity: {opacity:.2f};\n" + selector += "}\n" + return selector + + def write(self, output_file) -> None: + """ + Construct icon selectors for MapCSS 0.2 scheme. + """ + with Path("data/roentgen_icons_part.mapcss").open() as input_file: + output_file.write(input_file.read()) + + output_file.write("\n") + + for matcher in self.matchers: + for target in ["node", "area"]: + output_file.write(self.add_selector(target, matcher)) + + if not self.add_icons_for_lifecycle: + return + + for index, stage_of_decay in enumerate(STAGES_OF_DECAY): + opacity: float = 0.6 - 0.4 * index / (len(STAGES_OF_DECAY) - 1) + for matcher in self.matchers: + if len(matcher.tags) > 1: + continue + for target in ["node", "area"]: + output_file.write( + self.add_selector( + target, matcher, stage_of_decay, opacity + ) + ) def write_mapcss() -> None: @@ -58,12 +99,10 @@ def write_mapcss() -> None: collection.draw_icons( icons_with_outline_path, color=Color("black"), outline=True ) - with Path("data/roentgen_icons_part.mapcss").open() as input_file: - header = input_file.read() - + mapcss_writer: MapCSSWriter = MapCSSWriter( + scheme, icon_directory_name, True + ) with (directory / "roentgen_icons.mapcss").open("w+") as output_file: - output_file.write(header) - output_file.write("\n") - output_file.write(construct_selectors(scheme, icon_directory_name)) + mapcss_writer.write(output_file) logging.info(f"MapCSS 0.2 scheme is written to {directory}.") diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py index 53f3872..338dc34 100644 --- a/roentgen/osm_reader.py +++ b/roentgen/osm_reader.py @@ -23,6 +23,18 @@ KILOMETERS_PATTERN = re.compile("^(?P\\d*\\.?\\d*)\\s*km$") MILES_PATTERN = re.compile("^(?P\\d*\\.?\\d*)\\s*mi$") +STAGES_OF_DECAY: List[str] = [ + "disused", + "abandoned", + "ruins", + "demolished", + "removed", + "razed", + "destroyed", + "was", # is not actually a stage of decay +] + + def parse_float(string: str) -> Optional[float]: """ Parse string representation of a float or integer value. diff --git a/roentgen/scheme.py b/roentgen/scheme.py index 4af7474..7bad4dc 100644 --- a/roentgen/scheme.py +++ b/roentgen/scheme.py @@ -69,6 +69,17 @@ def is_matched_tag( return MatchingType.NOT_MATCHED +def get_selector(key: str, value: str, prefix: str = "") -> str: + """Get MapCSS 0.2 selector for one key.""" + if prefix: + key = f"{prefix}:{key}" + if value == "*": + return f"[{key}]" + if '"' in value: + return f"[{key}='{value}']" + return f'[{key}="{value}"]' + + class Matcher: """ Tag matching. @@ -120,21 +131,13 @@ class Matcher: return matched - def get_mapcss_selector(self) -> str: + def get_mapcss_selector(self, prefix: str = "") -> str: """ Construct MapCSS 0.2 selector from the node matcher. See https://wiki.openstreetmap.org/wiki/MapCSS/0.2 """ - def get_selector(key: str, value: str) -> str: - """Get MapCSS 0.2 selector for one key.""" - if value == "*": - return f"[{key}]" - if '"' in value: - return f"[{key}='{value}']" - return f'[{key}="{value}"]' - - return "".join([get_selector(x, y) for (x, y) in self.tags.items()]) + return "".join([get_selector(x, y, prefix) for (x, y) in self.tags.items()]) class NodeMatcher(Matcher):