Support color for icon shape.

* Create icon shape specification with color.
  * Change icon generation rules.
  * Add height label processing.
This commit is contained in:
Sergey Vartanov 2021-05-09 05:04:38 +03:00
parent a84e838a1b
commit bf4db29a1a
10 changed files with 203 additions and 195 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Before After
Before After

View file

@ -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")

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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]))

View file

@ -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]