Refactor point drawing.

Draw extra icons under main icon.
This commit is contained in:
Sergey Vartanov 2020-09-27 00:45:18 +03:00
parent 81199551a1
commit 7b58dae764
5 changed files with 320 additions and 285 deletions

View file

@ -4,7 +4,6 @@ Construct Röntgen nodes and ways.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
from collections import Counter from collections import Counter
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
@ -14,14 +13,15 @@ import numpy as np
from roentgen import ui from roentgen import ui
from roentgen.color import get_gradient_color from roentgen.color import get_gradient_color
from roentgen.icon import DEFAULT_SMALL_SHAPE_ID from roentgen.icon import DEFAULT_SMALL_SHAPE_ID, IconExtractor
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.osm_reader import ( from roentgen.osm_reader import (
Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged) Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged)
from roentgen.point import Point
from roentgen.scheme import IconSet, Scheme, LineStyle from roentgen.scheme import IconSet, Scheme, LineStyle
from roentgen.util import MinMax from roentgen.util import MinMax
DEBUG: bool = True DEBUG: bool = False
TIME_COLOR_SCALE: List[Color] = [ TIME_COLOR_SCALE: List[Color] = [
Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"), Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"),
Color("#FFC300"), Color("#DAF7A6")] Color("#FFC300"), Color("#DAF7A6")]
@ -60,27 +60,6 @@ def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
return polygon if not is_clockwise(polygon) else list(reversed(polygon)) return polygon if not is_clockwise(polygon) else list(reversed(polygon))
class Point(Tagged):
"""
Object on the map with no dimensional attributes.
"""
def __init__(
self, icon_set: IconSet, tags: Dict[str, str], point: np.array,
coordinates: np.array, priority: float = 0,
is_for_node: bool = True):
super().__init__()
assert point is not None
self.icon_set: IconSet = icon_set
self.tags: Dict[str, str] = tags
self.point: np.array = point
self.coordinates: np.array = coordinates
self.priority: float = priority
self.layer: float = 0
self.is_for_node: bool = is_for_node
class Figure(Tagged): class Figure(Tagged):
""" """
Some figure on the map: way or area. Some figure on the map: way or area.
@ -168,16 +147,6 @@ class Building(Figure):
return 3 return 3
@dataclass
class TextStruct:
"""
Some label on the map with attributes.
"""
text: str
fill: Color = Color("#444444")
size: float = 10
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array: def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
""" """
Get geometric center of nodes set. Get geometric center of nodes set.
@ -280,7 +249,7 @@ class Constructor:
""" """
def __init__( def __init__(
self, check_level, mode: str, seed: str, map_: Map, self, check_level, mode: str, seed: str, map_: Map,
flinger: Flinger, scheme: Scheme): flinger: Flinger, scheme: Scheme, icon_extractor: IconExtractor):
self.check_level = check_level self.check_level = check_level
self.mode: str = mode self.mode: str = mode
@ -288,6 +257,7 @@ class Constructor:
self.map_: Map = map_ self.map_: Map = map_
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.icon_extractor = icon_extractor
self.nodes: List[Point] = [] self.nodes: List[Point] = []
self.figures: List[Figure] = [] self.figures: List[Figure] = []
@ -331,8 +301,6 @@ class Constructor:
""" """
assert len(outers) >= 1 assert len(outers) >= 1
line_is_cycle: bool = is_cycle(outers[0])
center_point, center_coordinates = ( center_point, center_coordinates = (
line_center(outers[0], self.flinger)) line_center(outers[0], self.flinger))
@ -368,7 +336,8 @@ class Constructor:
else: else:
self.figures.append( self.figures.append(
Figure(line.tags, inners, outers, line_style)) Figure(line.tags, inners, outers, line_style))
icon_set: IconSet = self.scheme.get_icon(line.tags, for_="line") icon_set: IconSet = self.scheme.get_icon(
self.icon_extractor, line.tags, for_="line")
self.nodes.append(Point( self.nodes.append(Point(
icon_set, line.tags, center_point, center_coordinates, icon_set, line.tags, center_point, center_coordinates,
is_for_node=False)) is_for_node=False))
@ -380,7 +349,8 @@ class Constructor:
"stroke-width": 1} "stroke-width": 1}
self.figures.append(Figure( self.figures.append(Figure(
line.tags, inners, outers, LineStyle(style, 1000))) line.tags, inners, outers, LineStyle(style, 1000)))
icon_set: IconSet = self.scheme.get_icon(line.tags) icon_set: IconSet = self.scheme.get_icon(
self.icon_extractor, line.tags)
self.nodes.append(Point( self.nodes.append(Point(
icon_set, line.tags, center_point, center_coordinates, icon_set, line.tags, center_point, center_coordinates,
is_for_node=False)) is_for_node=False))
@ -433,7 +403,7 @@ class Constructor:
if not self.check_level(tags): if not self.check_level(tags):
continue continue
icon_set: IconSet = self.scheme.get_icon(tags) icon_set: IconSet = self.scheme.get_icon(self.icon_extractor, tags)
if self.mode in ["time", "user-coloring"]: if self.mode in ["time", "user-coloring"]:
if not tags: if not tags:
@ -450,7 +420,4 @@ class Constructor:
f"{key}: {tags[key]}" for key in tags f"{key}: {tags[key]}" for key in tags
if key not in icon_set.processed) if key not in icon_set.processed)
for t in missing_tags.most_common():
print(t)
ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes") ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes")

View file

@ -6,10 +6,12 @@ Author: Sergey Vartanov (me@enzet.ru).
import re import re
import xml.dom.minidom import xml.dom.minidom
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict from typing import Dict, Any
from xml.dom.minidom import Document, Element, Node from xml.dom.minidom import Document, Element, Node
import numpy as np import numpy as np
import svgwrite
from colour import Color
from svgwrite import Drawing from svgwrite import Drawing
from roentgen import ui from roentgen import ui
@ -49,6 +51,36 @@ class Icon:
return svg.path( return svg.path(
d=self.path, transform=f"translate({shift[0]},{shift[1]})") d=self.path, transform=f"translate({shift[0]},{shift[1]})")
def draw(
self, svg: svgwrite.Drawing, point: np.array, color: Color,
opacity=1.0, tags: Dict[str, Any] = None, outline: bool = False):
"""
Draw icon shape into SVG file.
:param svg: output SVG file
:param point: icon position
: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)
class IconExtractor: class IconExtractor:
""" """

View file

@ -14,13 +14,12 @@ from colour import Color
from svgwrite.container import Group from svgwrite.container import Group
from svgwrite.path import Path from svgwrite.path import Path
from svgwrite.shapes import Rect from svgwrite.shapes import Rect
from svgwrite.text import Text from typing import Any, Dict
from typing import Any, Dict, List, Optional
from roentgen import ui from roentgen import ui
from roentgen.address import get_address
from roentgen.constructor import ( from roentgen.constructor import (
Constructor, Point, Figure, TextStruct, Building, Segment) Constructor, Figure, Building, Segment)
from roentgen.point import Point
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.grid import draw_grid from roentgen.grid import draw_grid
from roentgen.icon import Icon, IconExtractor from roentgen.icon import Icon, IconExtractor
@ -29,7 +28,6 @@ from roentgen.osm_reader import Map, OSMReader
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.direction import DirectionSet, Sector from roentgen.direction import DirectionSet, Sector
from roentgen.util import MinMax from roentgen.util import MinMax
from roentgen.color import is_bright
ICONS_FILE_NAME: str = "icons/icons.svg" ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml" TAGS_FILE_NAME: str = "data/tags.yml"
@ -38,8 +36,6 @@ MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"
AUTHOR_MODE = "user-coloring" AUTHOR_MODE = "user-coloring"
CREATION_TIME_MODE = "time" CREATION_TIME_MODE = "time"
DEFAULT_FONT = "Roboto"
class Painter: class Painter:
""" """
@ -64,160 +60,6 @@ class Painter:
self.icon_extractor = icon_extractor self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
def draw_shapes(self, node: Point, points: List[List[float]]):
"""
Draw shapes for one node.
"""
if node.icon_set.is_default and not node.is_for_node:
return
left: float = -(len(node.icon_set.icons) - 1) * 8
if self.overlap != 0:
for shape_ids in node.icon_set.icons:
has_space = True
for p in points[-1000:]:
if node.point[0] + left - self.overlap <= p[0] \
<= node.point[0] + left + self.overlap and \
node.point[1] - self.overlap <= p[1] \
<= node.point[1] + self.overlap:
has_space = False
break
if has_space:
self.draw_point_shape(
shape_ids, (node.point[0] + left, node.point[1]),
node.icon_set.color, tags=node.tags)
points.append([node.point[0] + left, node.point[1]])
left += 16
else:
for shape_ids in node.icon_set.icons:
self.draw_point_shape(
shape_ids, (node.point[0] + left, node.point[1]),
node.icon_set.color, tags=node.tags)
left += 16
def draw_texts(self, node: Point):
"""
Draw all labels.
"""
text_y: float = 0
write_tags = self.construct_text(node.tags, node.icon_set.processed)
for text_struct in write_tags: # type: TextStruct
text_y += text_struct.size + 1
text = text_struct.text
text = text.replace("&quot;", '"')
text = text.replace("&amp;", '&')
text = text[:26] + ("..." if len(text) > 26 else "")
self.draw_text(
text, (node.point[0], node.point[1] + text_y + 8),
text_struct.fill, size=text_struct.size)
def draw_text(
self, text: str, point, fill: Color, size: float = 10,
out_fill=Color("white"), out_opacity=1.0,
out_fill_2: Optional[Color] = None, out_opacity_2=1.0):
"""
Drawing text.
###### ### outline 2
#------# --- outline 1
#| Text |#
#------#
######
"""
if out_fill_2:
self.svg.add(Text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=out_fill_2.hex,
stroke_linejoin="round", stroke_width=5,
stroke=out_fill_2.hex, opacity=out_opacity_2))
if out_fill:
self.svg.add(Text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=out_fill.hex,
stroke_linejoin="round", stroke_width=3,
stroke=out_fill.hex, opacity=out_opacity))
self.svg.add(Text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=fill.hex))
def construct_text(self, tags, processed) -> List[TextStruct]:
"""
Construct labels for not processed tags.
"""
texts: List[TextStruct] = []
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 += "бывш. " + tags["old_name"]
address: List[str] = get_address(tags, self.draw_captions)
if name:
texts.append(TextStruct(name, Color("black")))
if alt_name:
texts.append(TextStruct("(" + alt_name + ")"))
if address:
texts.append(TextStruct(", ".join(address)))
if self.draw_captions == "main":
return texts
if "route_ref" in tags:
texts.append(TextStruct(tags["route_ref"].replace(";", " ")))
tags.pop("route_ref", None)
if "cladr:code" in tags:
texts.append(TextStruct(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(TextStruct(link, Color("#000088")))
tags.pop("website", None)
for k in ["phone"]:
if k in tags:
texts.append(TextStruct(tags[k], Color("#444444")))
tags.pop(k)
for tag in tags:
if self.scheme.is_writable(tag) and not (tag in processed):
texts.append(TextStruct(tags[tag]))
return texts
def draw(self, constructor: Constructor, points): def draw(self, constructor: Constructor, points):
""" """
Draw map. Draw map.
@ -332,21 +174,21 @@ class Painter:
angle = float(node.get_tag("camera:angle")) angle = float(node.get_tag("camera:angle"))
if "angle" in node.tags: if "angle" in node.tags:
angle = float(node.get_tag("angle")) angle = float(node.get_tag("angle"))
direction_radius: float = \ direction_radius: float = (
25 * self.flinger.get_scale(node.coordinates) 25 * self.flinger.get_scale(node.coordinates))
direction_color: Color = \ direction_color: Color = (
self.scheme.get_color("direction_camera_color") self.scheme.get_color("direction_camera_color"))
elif node.get_tag("traffic_sign") == "stop": elif node.get_tag("traffic_sign") == "stop":
direction = node.get_tag("direction") direction = node.get_tag("direction")
direction_radius: float = \ direction_radius: float = (
25 * self.flinger.get_scale(node.coordinates) 25 * self.flinger.get_scale(node.coordinates))
direction_color: Color = Color("red") direction_color: Color = Color("red")
else: else:
direction = node.get_tag("direction") direction = node.get_tag("direction")
direction_radius: float = \ direction_radius: float = (
50 * self.flinger.get_scale(node.coordinates) 50 * self.flinger.get_scale(node.coordinates))
direction_color: Color = \ direction_color: Color = (
self.scheme.get_color("direction_view_color") self.scheme.get_color("direction_view_color"))
is_revert_gradient = True is_revert_gradient = True
if not direction: if not direction:
@ -375,16 +217,16 @@ class Painter:
d=["M", point] + path + ["L", point, "Z"], d=["M", point] + path + ["L", point, "Z"],
fill=gradient.get_paint_server())) fill=gradient.get_paint_server()))
# All other nodes # All other points
nodes = sorted(constructor.nodes, key=lambda x: x.layer) nodes = sorted(constructor.nodes, key=lambda x: x.layer)
for index, node in enumerate(nodes): # type: int, Point for index, node in enumerate(nodes): # type: int, Point
if node.get_tag("natural") == "tree" and \ if (node.get_tag("natural") == "tree" and
("diameter_crown" in node.tags or ("diameter_crown" in node.tags or
"circumference" in node.tags): "circumference" in node.tags)):
continue continue
ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes") ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes")
self.draw_shapes(node, points) node.draw_shapes(self.svg)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes") ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
if self.draw_captions == "no": if self.draw_captions == "no":
@ -392,52 +234,7 @@ class Painter:
for node in nodes: # type: Point for node in nodes: # type: Point
if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]: if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
self.draw_texts(node) node.draw_texts(self.svg, self.scheme, self.draw_captions)
def draw_point_shape(
self, shape_ids: List[str], point, fill: Color, tags=None):
"""
Draw one icon.
"""
if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
for shape_id in shape_ids: # type: str
icon, _ = self.icon_extractor.get_path(shape_id)
self.draw_point_outline(icon, point, fill, mode=self.mode)
for shape_id in shape_ids: # type: str
icon, _ = self.icon_extractor.get_path(shape_id)
self.draw_point(icon, point, fill, tags=tags)
def draw_point(
self, icon: Icon, point: (float, float), fill: Color,
tags: Dict[str, str] = None) -> None:
point = np.array(list(map(int, point)))
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
path: svgwrite.path.Path = icon.get_path(self.svg, point)
path.update({"fill": fill.hex})
path.set_desc(title=title)
self.svg.add(path)
def draw_point_outline(
self, icon: Icon, point, fill: Color, mode="default"):
point = np.array(list(map(int, point)))
opacity: float = 0.5
stroke_width: float = 2.2
outline_fill: Color = self.scheme.get_color("outline_color")
if mode not in [AUTHOR_MODE, CREATION_TIME_MODE] and is_bright(fill):
outline_fill = Color("black")
opacity = 0.7
path = icon.get_path(self.svg, point)
path.update({
"fill": outline_fill.hex, "opacity": opacity,
"stroke": outline_fill.hex, "stroke-width": stroke_width,
"stroke-linejoin": "round"})
self.svg.add(path)
def check_level_number(tags: Dict[str, Any], level: float): def check_level_number(tags: Dict[str, Any], level: float):
@ -479,6 +276,11 @@ def check_level_overground(tags: Dict[str, Any]) -> bool:
def main(argv) -> None: def main(argv) -> None:
"""
Röntgen entry point.
:param argv: command-line arguments
"""
if len(argv) == 2: if len(argv) == 2:
if argv[1] == "grid": if argv[1] == "grid":
draw_grid() draw_grid()
@ -532,8 +334,8 @@ def main(argv) -> None:
flinger: Flinger = Flinger(MinMax(min1, max1), options.scale) flinger: Flinger = Flinger(MinMax(min1, max1), options.scale)
size: np.array = flinger.size size: np.array = flinger.size
svg: svgwrite.Drawing = \ svg: svgwrite.Drawing = (
svgwrite.Drawing(options.output_file_name, size=size) svgwrite.Drawing(options.output_file_name, size=size))
svg.add(Rect((0, 0), size, fill=background_color)) svg.add(Rect((0, 0), size, fill=background_color))
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME) icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
@ -555,7 +357,8 @@ def main(argv) -> None:
return not check_level_number(x, float(options.level)) return not check_level_number(x, float(options.level))
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
check_level, options.mode, options.seed, map_, flinger, scheme) check_level, options.mode, options.seed, map_, flinger, scheme,
icon_extractor)
if options.draw_ways: if options.draw_ways:
constructor.construct_ways() constructor.construct_ways()
constructor.construct_relations() constructor.construct_relations()

219
roentgen/point.py Normal file
View file

@ -0,0 +1,219 @@
from dataclasses import dataclass
from typing import Dict, Optional, List
import numpy as np
import svgwrite
from colour import Color
from roentgen.address import get_address
from roentgen.color import is_bright
from roentgen.icon import Icon
from roentgen.osm_reader import Tagged
from roentgen.scheme import IconSet
DEFAULT_FONT: str = "Roboto"
DEFAULT_COLOR: Color = Color("#444444")
@dataclass
class TextStruct:
"""
Some label on the map with attributes.
"""
text: str
fill: Color = DEFAULT_COLOR
size: float = 10.0
def draw_point_shape(
svg: svgwrite.Drawing, icons: List[Icon], point, fill: Color,
tags=None):
"""
Draw one combined icon and its outline.
"""
# Down-cast floats to integers to make icons pixel-perfect.
point = np.array(list(map(int, point)))
# Draw outlines.
for icon in icons: # type: Icon
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, point, color, opacity=opacity, outline=True)
# Draw icons.
for icon in icons: # type: Icon
icon.draw(svg, point, fill, tags=tags)
def draw_text(
svg: svgwrite.Drawing, text: str, point, fill: Color,
size: float = 10.0, out_fill=Color("white"), out_opacity=1.0,
out_fill_2: Optional[Color] = None, out_opacity_2=1.0):
"""
Drawing text.
###### ### outline 2
#------# --- outline 1
#| Text |#
#------#
######
"""
if out_fill_2:
svg.add(svg.text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=out_fill_2.hex,
stroke_linejoin="round", stroke_width=5,
stroke=out_fill_2.hex, opacity=out_opacity_2))
if out_fill:
svg.add(svg.text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=out_fill.hex,
stroke_linejoin="round", stroke_width=3,
stroke=out_fill.hex, opacity=out_opacity))
svg.add(svg.text(
text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=fill.hex))
def construct_text(
tags, processed, scheme, draw_captions) -> List["TextStruct"]:
"""
Construct labels for not processed tags.
"""
texts: List[TextStruct] = []
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 += "бывш. " + tags["old_name"]
address: List[str] = get_address(tags, draw_captions)
if name:
texts.append(TextStruct(name, Color("black")))
if alt_name:
texts.append(TextStruct(f"({alt_name})"))
if address:
texts.append(TextStruct(", ".join(address)))
if draw_captions == "main":
return texts
if "route_ref" in tags:
texts.append(TextStruct(tags["route_ref"].replace(";", " ")))
tags.pop("route_ref", None)
if "cladr:code" in tags:
texts.append(TextStruct(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(TextStruct(link, Color("#000088")))
tags.pop("website", None)
for k in ["phone"]:
if k in tags:
texts.append(TextStruct(tags[k], Color("#444444")))
tags.pop(k)
for tag in tags:
if scheme.is_writable(tag) and not (tag in processed):
texts.append(TextStruct(tags[tag]))
return texts
class Point(Tagged):
"""
Object on the map with no dimensional attributes.
It may have icons and text.
"""
def __init__(
self, icon_set: IconSet, tags: Dict[str, str], point: np.array,
coordinates: np.array, priority: float = 0,
is_for_node: bool = True):
super().__init__()
assert point is not None
self.icon_set: IconSet = icon_set
self.tags: Dict[str, str] = tags
self.point: np.array = point
self.coordinates: np.array = coordinates
self.priority: float = priority
self.layer: float = 0
self.is_for_node: bool = is_for_node
self.y = 0
def draw_shapes(self, svg: svgwrite.Drawing):
"""
Draw shapes for one node.
"""
if self.icon_set.main_icon and (not self.icon_set.main_icon[0].is_default() or self.is_for_node):
draw_point_shape(
svg, self.icon_set.main_icon,
self.point + np.array((0, self.y)), self.icon_set.color,
tags=self.tags)
self.y += 16
left: float = -(len(self.icon_set.extra_icons) - 1) * 8
for shape_ids in self.icon_set.extra_icons:
draw_point_shape(
svg, shape_ids, self.point + np.array((left, self.y)),
Color("#888888"))
left += 16
if self.icon_set.extra_icons:
self.y += 16
def draw_texts(self, svg: svgwrite.Drawing, scheme, draw_captions):
"""
Draw all labels.
"""
write_tags = construct_text(
self.tags, self.icon_set.processed, scheme, draw_captions)
for text_struct in write_tags: # type: TextStruct
self.y += text_struct.size + 1
text = text_struct.text
text = text.replace("&quot;", '"')
text = text.replace("&amp;", '&')
text = text[:26] + ("..." if len(text) > 26 else "")
draw_text(
svg, text, self.point + np.array((0, self.y - 8)),
text_struct.fill, size=text_struct.size)

View file

@ -10,7 +10,7 @@ from colour import Color
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set, Union from typing import Any, Dict, List, Optional, Set, Union
from roentgen.icon import DEFAULT_SHAPE_ID from roentgen.icon import DEFAULT_SHAPE_ID, IconExtractor, Icon
DEFAULT_COLOR: Color = Color("#444444") DEFAULT_COLOR: Color = Color("#444444")
@ -20,7 +20,8 @@ class IconSet:
""" """
Node representation: icons and color. Node representation: icons and color.
""" """
icons: List[List[str]] # list of lists of shape identifiers main_icon: List[Icon] # list of icons
extra_icons: List[List[Icon]] # list of lists of icons
color: Color # fill color of all icons color: Color # fill color of all icons
# tag keys that were processed to create icon set (other # tag keys that were processed to create icon set (other
# tag keys should be displayed by text or ignored) # tag keys should be displayed by text or ignored)
@ -111,7 +112,9 @@ class Scheme:
return True return True
return False return False
def get_icon(self, tags: Dict[str, Any], for_: str = "node") -> IconSet: def get_icon(
self, icon_extractor: IconExtractor, tags: Dict[str, Any],
for_: str = "node") -> IconSet:
""" """
Construct icon set. Construct icon set.
@ -123,8 +126,8 @@ class Scheme:
if tags_hash in self.cache: if tags_hash in self.cache:
return self.cache[tags_hash] return self.cache[tags_hash]
main_icon: Optional[List[str]] = None main_icon_id: Optional[List[str]] = None
extra_icons: List[List[str]] = [] extra_icon_ids: List[List[str]] = []
processed: Set[str] = set() processed: Set[str] = set()
fill: Color = DEFAULT_COLOR fill: Color = DEFAULT_COLOR
@ -150,15 +153,15 @@ class Scheme:
if "draw" in matcher and not matcher["draw"]: if "draw" in matcher and not matcher["draw"]:
processed |= set(matcher["tags"].keys()) processed |= set(matcher["tags"].keys())
if "icon" in matcher: if "icon" in matcher:
main_icon = copy.deepcopy(matcher["icon"]) main_icon_id = copy.deepcopy(matcher["icon"])
processed |= set(matcher["tags"].keys()) processed |= set(matcher["tags"].keys())
if "over_icon" in matcher: if "over_icon" in matcher:
if main_icon: # TODO: check main icon in under icons if main_icon_id: # TODO: check main icon in under icons
main_icon += matcher["over_icon"] main_icon_id += matcher["over_icon"]
for key in matcher["tags"].keys(): for key in matcher["tags"].keys():
processed.add(key) processed.add(key)
if "add_icon" in matcher: if "add_icon" in matcher:
extra_icons += [matcher["add_icon"]] extra_icon_ids += [matcher["add_icon"]]
for key in matcher["tags"].keys(): for key in matcher["tags"].keys():
processed.add(key) processed.add(key)
if "color" in matcher: if "color" in matcher:
@ -167,26 +170,37 @@ class Scheme:
processed.add(key) processed.add(key)
for tag_key in tags: # type: str for tag_key in tags: # type: str
if (tag_key in ["color", "colour"] or tag_key.endswith(":color") or if (tag_key.endswith(":color") or
tag_key.endswith(":colour")): tag_key.endswith(":colour")):
fill = self.get_color(tags[tag_key]) fill = self.get_color(tags[tag_key])
processed.add(tag_key) processed.add(tag_key)
if main_icon: for tag_key in tags: # type: str
result_set: List[List[str]] = [main_icon] + extra_icons if tag_key in ["color", "colour"]:
else: fill = self.get_color(tags[tag_key])
result_set: List[List[str]] = extra_icons processed.add(tag_key)
keys_left = list(filter( keys_left = list(filter(
lambda x: x not in processed and lambda x: x not in processed and
not self.is_no_drawable(x), tags.keys())) not self.is_no_drawable(x), tags.keys()))
is_default: bool = False is_default: bool = False
if not result_set and keys_left: if not main_icon_id and not extra_icon_ids and keys_left:
result_set = [[DEFAULT_SHAPE_ID]] main_icon_id = [DEFAULT_SHAPE_ID]
is_default = True is_default = True
returned: IconSet = IconSet(result_set, fill, processed, is_default) main_icon: List[Icon] = []
if main_icon_id:
main_icon = list(map(
lambda x: icon_extractor.get_path(x)[0], main_icon_id))
extra_icons: List[List[Icon]] = []
for icon_id in extra_icon_ids:
extra_icons.append(list(map(
lambda x: icon_extractor.get_path(x)[0], icon_id)))
returned: IconSet = IconSet(
main_icon, extra_icons, fill, processed, is_default)
self.cache[tags_hash] = returned self.cache[tags_hash] = returned