diff --git a/doc/buildings.png b/doc/buildings.png index 254a2ac..55c827c 100644 Binary files a/doc/buildings.png and b/doc/buildings.png differ diff --git a/doc/grid.png b/doc/grid.png index aeeac57..0e44491 100644 Binary files a/doc/grid.png and b/doc/grid.png differ diff --git a/doc/power.png b/doc/power.png index bbfcc1e..40418d6 100644 Binary files a/doc/power.png and b/doc/power.png differ diff --git a/doc/surveillance.png b/doc/surveillance.png index 3af67ad..af058ce 100644 Binary files a/doc/surveillance.png and b/doc/surveillance.png differ diff --git a/roentgen/constructor.py b/roentgen/constructor.py index cc70303..723d1c1 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -19,7 +19,8 @@ from roentgen.osm_reader import ( Map, OSMMember, OSMNode, OSMRelation, OSMWay, Tagged ) from roentgen.point import Point -from roentgen.scheme import Icon, LineStyle, Scheme +from roentgen.scheme import Icon, LineStyle, Scheme, ShapeSpecification, \ + DEFAULT_COLOR from roentgen.util import MinMax DEBUG: bool = False @@ -445,34 +446,35 @@ class Constructor: continue priority: int - icon_set: Icon + icon: Icon draw_outline: bool = True if self.mode in ["time", "user-coloring"]: if not tags: continue + color = DEFAULT_COLOR if self.mode == "user-coloring": color = get_user_color(node.user, self.seed) if self.mode == "time": color = get_time_color(node.timestamp, self.map_.time) dot, _ = self.icon_extractor.get_path(DEFAULT_SMALL_SHAPE_ID) - icon_set = Icon([dot], [], color, set(), True) + icon = Icon([ShapeSpecification(dot, color)], [], set()) priority = 0 draw_outline = False labels = [] else: - icon_set, priority = self.scheme.get_icon( + icon, priority = self.scheme.get_icon( self.icon_extractor, tags ) labels = self.scheme.construct_text(tags, True) self.nodes.append(Point( - icon_set, labels, tags, flung, node.coordinates, + icon, labels, tags, flung, node.coordinates, priority=priority, draw_outline=draw_outline )) missing_tags.update( f"{key}: {tags[key]}" for key in tags - if key not in icon_set.processed) + if key not in icon.processed) ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes") diff --git a/roentgen/grid.py b/roentgen/grid.py index 3c0456d..ee88f99 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -11,7 +11,7 @@ from colour import Color from svgwrite import Drawing from roentgen.icon import IconExtractor, Shape -from roentgen.scheme import Scheme +from roentgen.scheme import Scheme, ShapeSpecification def draw_all_icons( @@ -35,9 +35,18 @@ def draw_all_icons( to_draw: List[Set[str]] = [] + icons_file_name: str = "icons/icons.svg" + extractor: IconExtractor = IconExtractor(icons_file_name) + for element in scheme.icons: # type: Dict[str, Any] - if "icon" in element and set(element["icon"]) not in to_draw: - to_draw.append(set(element["icon"])) + if "icon" in element: + specifications = [ + ShapeSpecification.from_structure(x, extractor, scheme) + for x in element["icon"] + ] + ids = set(x.shape.id_ for x in specifications) + if ids not in to_draw: + to_draw.append(ids) if "add_icon" in element and set(element["add_icon"]) not in to_draw: to_draw.append(set(element["add_icon"])) if "over_icon" not in element: @@ -65,16 +74,13 @@ def draw_all_icons( current_set not in to_draw): to_draw.append(current_set) - icons_file_name: str = "icons/icons.svg" - extractor: IconExtractor = IconExtractor(icons_file_name) - specified_ids: Set[str] = set() for icons_to_draw in to_draw: # type: List[str] specified_ids |= icons_to_draw print( "Icons with no tag specification: \n " + - ", ".join(sorted(extractor.icons.keys() - specified_ids)) + "." + ", ".join(sorted(extractor.shapes.keys() - specified_ids)) + "." ) draw_grid( diff --git a/roentgen/icon.py b/roentgen/icon.py index fcb1855..d23ab73 100644 --- a/roentgen/icon.py +++ b/roentgen/icon.py @@ -5,12 +5,10 @@ Author: Sergey Vartanov (me@enzet.ru). """ import re from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Dict, Optional from xml.dom.minidom import Document, Element, Node, parse import numpy as np -import svgwrite -from colour import Color from svgwrite import Drawing from roentgen import ui @@ -55,56 +53,6 @@ class Shape: d=self.path, transform=f"translate({shift[0]},{shift[1]})" ) - def draw( - self, svg: svgwrite.Drawing, point: np.array, color: Color, - opacity: float = 1.0, tags: Dict[str, Any] = None, outline: bool = False - ) -> None: - """ - Draw icon shape into SVG file. - - :param svg: output SVG file - :param point: 2D position of the icon centre - :param color: fill color - :param opacity: icon opacity - :param tags: tags to be displayed as hint - :param outline: draw outline for the icon - """ - point = np.array(list(map(int, point))) - - path: svgwrite.path.Path = self.get_path(svg, point) - path.update({"fill": color.hex}) - if outline: - opacity: float = 0.5 - - path.update({ - "fill": color.hex, - "stroke": color.hex, - "stroke-width": 2.2, - "stroke-linejoin": "round", - }) - if opacity != 1.0: - path.update({"opacity": opacity}) - if tags: - title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags)) - path.set_desc(title=title) - svg.add(path) - - -def is_standard(id_: str): - """ - Check whether SVG object ID is standard Inkscape ID. - """ - matcher = re.match(STANDARD_INKSCAPE_ID, id_) - return matcher is not None - if id_ == "base": - return True - for prefix in [ - ]: - matcher = re.match(prefix + "\\d+", id_) - if matcher: - return True - return False - class IconExtractor: """ @@ -118,10 +66,10 @@ class IconExtractor: :param svg_file_name: input SVG file name with icons. File may contain any other irrelevant graphics. """ - self.icons: Dict[str, Shape] = {} + self.shapes: Dict[str, Shape] = {} with open(svg_file_name) as input_file: - content = parse(input_file) # type: Document + content: Document = parse(input_file) for element in content.childNodes: # type: Element if element.nodeName != "svg": continue @@ -145,7 +93,7 @@ class IconExtractor: return id_: str = node.getAttribute("id") - if is_standard(id_): + if re.match(STANDARD_INKSCAPE_ID, id_) is not None: return if node.hasAttribute("d"): @@ -172,7 +120,7 @@ class IconExtractor: name = child_node.childNodes[0].nodeValue break - self.icons[id_] = Shape(path, point, id_, name) + self.shapes[id_] = Shape(path, point, id_, name) else: ui.error(f"not standard ID {id_}") @@ -182,8 +130,8 @@ class IconExtractor: :param id_: string icon identifier """ - if id_ in self.icons: - return self.icons[id_], True + if id_ in self.shapes: + return self.shapes[id_], True - ui.error(f"no such icon ID {id_}") - return self.icons[DEFAULT_SHAPE_ID], False + ui.error(f"no such shape ID {id_}") + return self.shapes[DEFAULT_SHAPE_ID], False diff --git a/roentgen/point.py b/roentgen/point.py index 1f77633..cf19f8e 100644 --- a/roentgen/point.py +++ b/roentgen/point.py @@ -7,7 +7,7 @@ from colour import Color from roentgen.color import is_bright from roentgen.icon import Shape from roentgen.osm_reader import Tagged -from roentgen.scheme import Icon +from roentgen.scheme import Icon, ShapeSpecification from roentgen.text import Label DEFAULT_FONT: str = "Roboto" @@ -71,17 +71,17 @@ class Point(Tagged): Draw main shape for one node. """ if ( - self.icon.main_icon and - (not self.icon.main_icon[0].is_default() or - self.is_for_node) + self.icon.main_icon[0].is_default() and + not self.icon.extra_icons ): - position = self.point + np.array((0, self.y)) - self.main_icon_painted: bool = self.draw_point_shape( - svg, self.icon.main_icon, - position, self.icon.color, occupied, - tags=self.tags) - if self.main_icon_painted: - self.y += 16 + return + + position = self.point + np.array((0, self.y)) + self.main_icon_painted: bool = self.draw_point_shape( + svg, self.icon.main_icon, position, occupied, tags=self.tags + ) + if self.main_icon_painted: + self.y += 16 def draw_extra_shapes( self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None @@ -108,14 +108,14 @@ class Point(Tagged): for shape_ids in self.icon.extra_icons: self.draw_point_shape( svg, shape_ids, self.point + np.array((left, self.y)), - Color("#888888"), occupied) + occupied=occupied) left += 16 if self.icon.extra_icons: self.y += 16 def draw_point_shape( - self, svg: svgwrite.Drawing, shapes: List[Shape], position, - fill: Color, occupied, tags: Optional[Dict[str, str]] = None + self, svg: svgwrite.Drawing, shapes: List[ShapeSpecification], position, + occupied, tags: Optional[Dict[str, str]] = None ) -> bool: """ Draw one combined icon and its outline. @@ -129,16 +129,13 @@ class Point(Tagged): # Draw outlines. if self.draw_outline: - for icon in shapes: # type: Shape - bright: bool = is_bright(fill) - color: Color = Color("black") if bright else Color("white") - opacity: float = 0.7 if bright else 0.5 - icon.draw(svg, position, color, opacity=opacity, outline=True) + for shape in shapes: + shape.draw(svg, position, outline=True) # Draw icons. - for icon in shapes: # type: Shape - icon.draw(svg, position, fill, tags=tags) + for shape in shapes: # type: ShapeSpecification + shape.draw(svg, position, tags=tags) if occupied: overlap: int = occupied.overlap diff --git a/roentgen/scheme.py b/roentgen/scheme.py index e9cb008..0289a1c 100644 --- a/roentgen/scheme.py +++ b/roentgen/scheme.py @@ -3,32 +3,102 @@ Röntgen drawing scheme. Author: Sergey Vartanov (me@enzet.ru). """ -import copy from dataclasses import dataclass from enum import Enum from typing import Any, Dict, List, Optional, Set, Tuple, Union +import numpy as np +import svgwrite import yaml from colour import Color +from roentgen.color import is_bright from roentgen.icon import DEFAULT_SHAPE_ID, IconExtractor, Shape from roentgen.text import Label, get_address, get_text DEFAULT_COLOR: Color = Color("#444444") +@dataclass +class ShapeSpecification: + """ + Specification for shape as a part of an icon. + """ + + shape: Shape + color: Color = DEFAULT_COLOR + + @classmethod + def from_structure( + cls, structure: Any, extractor: IconExtractor, scheme: "Scheme", + color: Color = DEFAULT_COLOR + ) -> "ShapeSpecification": + """ + Parse shape specification from structure. + """ + shape: Shape + shape, _ = extractor.get_path(DEFAULT_SHAPE_ID) + color: Color = color + if isinstance(structure, str): + shape, _ = extractor.get_path(structure) + elif isinstance(structure, dict): + if "shape" in structure: + shape, _ = extractor.get_path(structure["shape"]) + if "color" in structure: + color = scheme.get_color(structure["color"]) + return cls(shape, color) + + def is_default(self) -> bool: + """ + Check whether shape is default. + """ + return self.shape.id_ == DEFAULT_SHAPE_ID + + def draw( + self, svg: svgwrite.Drawing, point: np.array, + tags: Dict[str, Any] = None, outline: bool = False + ) -> None: + """ + Draw icon shape into SVG file. + + :param svg: output SVG file + :param point: 2D position of the icon centre + :param opacity: icon opacity + :param tags: tags to be displayed as hint + :param outline: draw outline for the icon + """ + point = np.array(list(map(int, point))) + + path: svgwrite.path.Path = self.shape.get_path(svg, point) + path.update({"fill": self.color.hex}) + if outline: + bright: bool = is_bright(self.color) + color: Color = Color("black") if bright else Color("white") + opacity: float = 0.7 if bright else 0.5 + + path.update({ + "fill": color.hex, + "stroke": color.hex, + "stroke-width": 2.2, + "stroke-linejoin": "round", + "opacity": opacity, + }) + if tags: + title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags)) + path.set_desc(title=title) + svg.add(path) + + @dataclass class Icon: """ Node representation: icons and color. """ - main_icon: List[Shape] # list of icons - extra_icons: List[List[Shape]] # list of lists of icons - color: Color # fill color of all icons + main_icon: List[ShapeSpecification] # list of shapes + extra_icons: List[List[ShapeSpecification]] # list of lists of shapes # tag keys that were processed to create icon set (other # tag keys should be displayed by text or ignored) processed: Set[str] - is_default: bool @dataclass @@ -148,9 +218,7 @@ class Scheme: try: return Color(color) except ValueError: - pass - - return DEFAULT_COLOR + return DEFAULT_COLOR def is_no_drawable(self, key: str) -> bool: """ @@ -199,72 +267,79 @@ class Scheme: if tags_hash in self.cache: return self.cache[tags_hash] - main_icon_id: Optional[List[str]] = None - extra_icon_ids: List[List[str]] = [] + main_icon: List[ShapeSpecification] = [] + extra_icons: List[List[ShapeSpecification]] = [] processed: Set[str] = set() - fill: Color = DEFAULT_COLOR priority: int = 0 for index, matcher in enumerate(self.icons): - # type: (int, Dict[str, Any]) + 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"].keys()) + processed |= set(matcher_tags) if "icon" in matcher: - main_icon_id = copy.deepcopy(matcher["icon"]) - processed |= set(matcher["tags"].keys()) + main_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_id: # TODO: check main icon in under icons - main_icon_id += matcher["over_icon"] - for key in matcher["tags"].keys(): + if main_icon: + main_icon += [ + 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_icon_ids += [matcher["add_icon"]] - for key in matcher["tags"].keys(): + extra_icons += [[ + 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: - fill = self.get_color(matcher["color"]) - for key in matcher["tags"].keys(): - processed.add(key) + assert False + if "set_main_color" in matcher: + for shape in main_icon: + shape.color = 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")): - fill = self.get_color(tags[tag_key]) + color = self.get_color(tags[tag_key]) processed.add(tag_key) for tag_key in tags: # type: str if tag_key in ["color", "colour"]: - fill = self.get_color(tags[tag_key]) + color = self.get_color(tags[tag_key]) processed.add(tag_key) - keys_left = list(filter( - lambda x: x not in processed and - not self.is_no_drawable(x), tags.keys() - )) + if color: + for shape_specification in main_icon: + shape_specification.color = color - is_default: bool = False - if not main_icon_id and not extra_icon_ids and keys_left: - main_icon_id = [DEFAULT_SHAPE_ID] - is_default = True + keys_left = [ + x for x in tags.keys() + if x not in processed and not self.is_no_drawable(x) + ] - main_icon: List[Shape] = [] - if main_icon_id: - main_icon = list(map( - lambda x: icon_extractor.get_path(x)[0], main_icon_id - )) + default_shape, _ = icon_extractor.get_path(DEFAULT_SHAPE_ID) + if not main_icon: + main_icon = [ShapeSpecification(default_shape)] - extra_icons: List[List[Shape]] = [] - for icon_id in extra_icon_ids: - extra_icons.append(list(map( - lambda x: icon_extractor.get_path(x)[0], icon_id))) - - returned: Icon = Icon( - main_icon, extra_icons, fill, processed, is_default - ) + returned: Icon = Icon(main_icon, extra_icons, processed) self.cache[tags_hash] = returned, priority return returned, priority @@ -283,8 +358,9 @@ class Scheme: priority = element["priority"] for key in element: # type: str if key not in [ - "tags", "no_tags", "priority", "level", "icon", - "r", "r1", "r2"]: + "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) @@ -374,6 +450,9 @@ class Scheme: if k in tags: texts.append(Label(tags[k], Color("#444444"))) tags.pop(k) + 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])) diff --git a/scheme/default.yml b/scheme/default.yml index 0d2add7..a9c2883 100644 --- a/scheme/default.yml +++ b/scheme/default.yml @@ -141,31 +141,24 @@ node_icons: icon: [electricity] # plant=* - tags: {plant: christmas_trees} - icon: [christmas_tree] - color: orchard_border_color + icon: [{shape: christmas_tree, color: orchard_border_color}] # produce=* - tags: {produce: apple} - icon: [apple] - color: orchard_border_color + icon: [{shape: apple, color: orchard_border_color}] - tags: {produce: christmas_trees} - icon: [christmas_tree] - color: orchard_border_color + icon: [{shape: christmas_tree, color: orchard_border_color}] - tags: {produce: pear} - icon: [pear] - color: orchard_border_color + icon: [{shape: pear, color: orchard_border_color}] # trees=* - tags: {trees: apple_trees} - icon: [apple] - color: orchard_border_color + icon: [{shape: apple, color: orchard_border_color}] - tags: {trees: pear_trees} - icon: [pear] - color: orchard_border_color + icon: [{shape: pear, color: orchard_border_color}] # Bigger objects - tags: {waterway: waterfall} - icon: [waterfall] - color: water_border_color + icon: [{shape: waterfall, color: water_border_color}] - tags: {natural: cliff} icon: [cliff] - tags: {natural: peak} @@ -361,19 +354,15 @@ node_icons: # Emergency - tags: {emergency: defibrillator} - icon: [defibrillator] - color: emergency_color + icon: [{shape: defibrillator, color: emergency_color}] - tags: {emergency: fire_extinguisher} - icon: [fire_extinguisher] - color: emergency_color + icon: [{shape: fire_extinguisher, color: emergency_color}] - tags: {emergency: fire_hydrant} icon: [fire_hydrant] - tags: {emergency: life_ring} - icon: [life_ring] - color: emergency_color + icon: [{shape: life_ring, color: emergency_color}] - tags: {emergency: phone} - icon: [sos_phone] - color: emergency_color + icon: [{shape: sos_phone, color: emergency_color}] # Transport-important middle objects @@ -628,8 +617,7 @@ node_icons: - tags: {amenity: clock} icon: [clock] - tags: {amenity: fountain} - icon: [fountain] - color: water_border_color + icon: [{shape: fountain, color: water_border_color}] - tags: {amenity: waste_basket} icon: [waste_basket] - tags: {highway: street_lamp} @@ -666,50 +654,38 @@ node_icons: icon: [lowered_kerb] # Trees - tags: {natural: tree} - icon: [tree] - color: tree_color + icon: [{shape: tree, color: tree_color}] - tags: {leaf_type: broadleaved} - icon: [tree_with_leaf] - color: tree_color + icon: [{shape: tree_with_leaf, color: tree_color}] - tags: {leaf_type: needleleaved} - icon: [needleleaved_tree] - color: tree_color + icon: [{shape: needleleaved_tree, color: tree_color}] - tags: {leaf_type: palm} - icon: [palm] - color: tree_color + icon: [{shape: palm, color: tree_color}] - tags: {natural: tree, leaf_type: broadleaved} - icon: [tree_with_leaf] - color: tree_color + icon: [{shape: tree_with_leaf, color: tree_color}] - tags: {natural: tree, leaf_type: needleleaved} - icon: [needleleaved_tree] - color: tree_color + icon: [{shape: needleleaved_tree, color: tree_color}] - tags: {natural: tree, leaf_type: palm} - icon: [palm] - color: tree_color + icon: [{shape: palm, color: tree_color}] - tags: {natural: tree, type: conifer} - icon: [needleleaved_tree] - color: tree_color + icon: [{shape: needleleaved_tree, color: tree_color}] - tags: {leaf_cycle: deciduous} - color: decidious_color + set_main_color: decidious_color - tags: {leaf_cycle: evergreen} - color: evergreen_color + set_main_color: evergreen_color - tags: {natural: tree, leaf_cycle: deciduous} - color: decidious_color + set_main_color: decidious_color - tags: {natural: tree, leaf_cycle: evergreen} - color: evergreen_color + set_main_color: evergreen_color - tags: {natural: bush} - icon: [bush] - color: tree_color + icon: [{shape: bush, color: tree_color}] # Tree genus - tags: {natural: tree, genus: Betula} - icon: [betula] - color: tree_color + icon: [{shape: betula, color: tree_color}] - tags: {natural: tree, "genus:en": Birch} - icon: [betula] - color: tree_color + icon: [{shape: betula, color: tree_color}] - tags: {natural: tree, "genus:ru": Берёза} - icon: [betula] - color: tree_color + icon: [{shape: betula, color: tree_color}] - tags: {railway: buffer_stop} icon: [buffer_stop]