mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-21 13:06:25 +02:00
Refactor point drawing.
Draw extra icons under main icon.
This commit is contained in:
parent
81199551a1
commit
7b58dae764
5 changed files with 320 additions and 285 deletions
|
@ -4,7 +4,6 @@ Construct Röntgen nodes and ways.
|
|||
Author: Sergey Vartanov (me@enzet.ru).
|
||||
"""
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
@ -14,14 +13,15 @@ import numpy as np
|
|||
|
||||
from roentgen import ui
|
||||
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.osm_reader import (
|
||||
Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged)
|
||||
from roentgen.point import Point
|
||||
from roentgen.scheme import IconSet, Scheme, LineStyle
|
||||
from roentgen.util import MinMax
|
||||
|
||||
DEBUG: bool = True
|
||||
DEBUG: bool = False
|
||||
TIME_COLOR_SCALE: List[Color] = [
|
||||
Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"),
|
||||
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))
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Some figure on the map: way or area.
|
||||
|
@ -168,16 +147,6 @@ class Building(Figure):
|
|||
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:
|
||||
"""
|
||||
Get geometric center of nodes set.
|
||||
|
@ -280,7 +249,7 @@ class Constructor:
|
|||
"""
|
||||
def __init__(
|
||||
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.mode: str = mode
|
||||
|
@ -288,6 +257,7 @@ class Constructor:
|
|||
self.map_: Map = map_
|
||||
self.flinger: Flinger = flinger
|
||||
self.scheme: Scheme = scheme
|
||||
self.icon_extractor = icon_extractor
|
||||
|
||||
self.nodes: List[Point] = []
|
||||
self.figures: List[Figure] = []
|
||||
|
@ -331,8 +301,6 @@ class Constructor:
|
|||
"""
|
||||
assert len(outers) >= 1
|
||||
|
||||
line_is_cycle: bool = is_cycle(outers[0])
|
||||
|
||||
center_point, center_coordinates = (
|
||||
line_center(outers[0], self.flinger))
|
||||
|
||||
|
@ -368,7 +336,8 @@ class Constructor:
|
|||
else:
|
||||
self.figures.append(
|
||||
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(
|
||||
icon_set, line.tags, center_point, center_coordinates,
|
||||
is_for_node=False))
|
||||
|
@ -380,7 +349,8 @@ class Constructor:
|
|||
"stroke-width": 1}
|
||||
self.figures.append(Figure(
|
||||
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(
|
||||
icon_set, line.tags, center_point, center_coordinates,
|
||||
is_for_node=False))
|
||||
|
@ -433,7 +403,7 @@ class Constructor:
|
|||
if not self.check_level(tags):
|
||||
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 not tags:
|
||||
|
@ -450,7 +420,4 @@ class Constructor:
|
|||
f"{key}: {tags[key]}" for key in tags
|
||||
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")
|
||||
|
|
|
@ -6,10 +6,12 @@ Author: Sergey Vartanov (me@enzet.ru).
|
|||
import re
|
||||
import xml.dom.minidom
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
from typing import Dict, Any
|
||||
from xml.dom.minidom import Document, Element, Node
|
||||
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
from colour import Color
|
||||
from svgwrite import Drawing
|
||||
|
||||
from roentgen import ui
|
||||
|
@ -49,6 +51,36 @@ class Icon:
|
|||
return svg.path(
|
||||
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:
|
||||
"""
|
||||
|
|
|
@ -14,13 +14,12 @@ from colour import Color
|
|||
from svgwrite.container import Group
|
||||
from svgwrite.path import Path
|
||||
from svgwrite.shapes import Rect
|
||||
from svgwrite.text import Text
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
from roentgen import ui
|
||||
from roentgen.address import get_address
|
||||
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.grid import draw_grid
|
||||
from roentgen.icon import Icon, IconExtractor
|
||||
|
@ -29,7 +28,6 @@ from roentgen.osm_reader import Map, OSMReader
|
|||
from roentgen.scheme import Scheme
|
||||
from roentgen.direction import DirectionSet, Sector
|
||||
from roentgen.util import MinMax
|
||||
from roentgen.color import is_bright
|
||||
|
||||
ICONS_FILE_NAME: str = "icons/icons.svg"
|
||||
TAGS_FILE_NAME: str = "data/tags.yml"
|
||||
|
@ -38,8 +36,6 @@ MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"
|
|||
AUTHOR_MODE = "user-coloring"
|
||||
CREATION_TIME_MODE = "time"
|
||||
|
||||
DEFAULT_FONT = "Roboto"
|
||||
|
||||
|
||||
class Painter:
|
||||
"""
|
||||
|
@ -64,160 +60,6 @@ class Painter:
|
|||
self.icon_extractor = icon_extractor
|
||||
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(""", '"')
|
||||
text = text.replace("&", '&')
|
||||
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):
|
||||
"""
|
||||
Draw map.
|
||||
|
@ -332,21 +174,21 @@ class Painter:
|
|||
angle = float(node.get_tag("camera:angle"))
|
||||
if "angle" in node.tags:
|
||||
angle = float(node.get_tag("angle"))
|
||||
direction_radius: float = \
|
||||
25 * self.flinger.get_scale(node.coordinates)
|
||||
direction_color: Color = \
|
||||
self.scheme.get_color("direction_camera_color")
|
||||
direction_radius: float = (
|
||||
25 * self.flinger.get_scale(node.coordinates))
|
||||
direction_color: Color = (
|
||||
self.scheme.get_color("direction_camera_color"))
|
||||
elif node.get_tag("traffic_sign") == "stop":
|
||||
direction = node.get_tag("direction")
|
||||
direction_radius: float = \
|
||||
25 * self.flinger.get_scale(node.coordinates)
|
||||
direction_radius: float = (
|
||||
25 * self.flinger.get_scale(node.coordinates))
|
||||
direction_color: Color = Color("red")
|
||||
else:
|
||||
direction = node.get_tag("direction")
|
||||
direction_radius: float = \
|
||||
50 * self.flinger.get_scale(node.coordinates)
|
||||
direction_color: Color = \
|
||||
self.scheme.get_color("direction_view_color")
|
||||
direction_radius: float = (
|
||||
50 * self.flinger.get_scale(node.coordinates))
|
||||
direction_color: Color = (
|
||||
self.scheme.get_color("direction_view_color"))
|
||||
is_revert_gradient = True
|
||||
|
||||
if not direction:
|
||||
|
@ -375,16 +217,16 @@ class Painter:
|
|||
d=["M", point] + path + ["L", point, "Z"],
|
||||
fill=gradient.get_paint_server()))
|
||||
|
||||
# All other nodes
|
||||
# All other points
|
||||
|
||||
nodes = sorted(constructor.nodes, key=lambda x: x.layer)
|
||||
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
|
||||
"circumference" in node.tags):
|
||||
"circumference" in node.tags)):
|
||||
continue
|
||||
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")
|
||||
|
||||
if self.draw_captions == "no":
|
||||
|
@ -392,52 +234,7 @@ class Painter:
|
|||
|
||||
for node in nodes: # type: Point
|
||||
if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
|
||||
self.draw_texts(node)
|
||||
|
||||
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)
|
||||
node.draw_texts(self.svg, self.scheme, self.draw_captions)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Röntgen entry point.
|
||||
|
||||
:param argv: command-line arguments
|
||||
"""
|
||||
if len(argv) == 2:
|
||||
if argv[1] == "grid":
|
||||
draw_grid()
|
||||
|
@ -532,8 +334,8 @@ def main(argv) -> None:
|
|||
flinger: Flinger = Flinger(MinMax(min1, max1), options.scale)
|
||||
size: np.array = flinger.size
|
||||
|
||||
svg: svgwrite.Drawing = \
|
||||
svgwrite.Drawing(options.output_file_name, size=size)
|
||||
svg: svgwrite.Drawing = (
|
||||
svgwrite.Drawing(options.output_file_name, size=size))
|
||||
svg.add(Rect((0, 0), size, fill=background_color))
|
||||
|
||||
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
|
||||
|
@ -555,7 +357,8 @@ def main(argv) -> None:
|
|||
return not check_level_number(x, float(options.level))
|
||||
|
||||
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:
|
||||
constructor.construct_ways()
|
||||
constructor.construct_relations()
|
||||
|
|
219
roentgen/point.py
Normal file
219
roentgen/point.py
Normal 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(""", '"')
|
||||
text = text.replace("&", '&')
|
||||
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)
|
|
@ -10,7 +10,7 @@ from colour import Color
|
|||
from dataclasses import dataclass
|
||||
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")
|
||||
|
||||
|
@ -20,7 +20,8 @@ class IconSet:
|
|||
"""
|
||||
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
|
||||
# tag keys that were processed to create icon set (other
|
||||
# tag keys should be displayed by text or ignored)
|
||||
|
@ -111,7 +112,9 @@ class Scheme:
|
|||
return True
|
||||
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.
|
||||
|
||||
|
@ -123,8 +126,8 @@ class Scheme:
|
|||
if tags_hash in self.cache:
|
||||
return self.cache[tags_hash]
|
||||
|
||||
main_icon: Optional[List[str]] = None
|
||||
extra_icons: List[List[str]] = []
|
||||
main_icon_id: Optional[List[str]] = None
|
||||
extra_icon_ids: List[List[str]] = []
|
||||
processed: Set[str] = set()
|
||||
fill: Color = DEFAULT_COLOR
|
||||
|
||||
|
@ -150,15 +153,15 @@ class Scheme:
|
|||
if "draw" in matcher and not matcher["draw"]:
|
||||
processed |= set(matcher["tags"].keys())
|
||||
if "icon" in matcher:
|
||||
main_icon = copy.deepcopy(matcher["icon"])
|
||||
main_icon_id = copy.deepcopy(matcher["icon"])
|
||||
processed |= set(matcher["tags"].keys())
|
||||
if "over_icon" in matcher:
|
||||
if main_icon: # TODO: check main icon in under icons
|
||||
main_icon += matcher["over_icon"]
|
||||
if main_icon_id: # TODO: check main icon in under icons
|
||||
main_icon_id += matcher["over_icon"]
|
||||
for key in matcher["tags"].keys():
|
||||
processed.add(key)
|
||||
if "add_icon" in matcher:
|
||||
extra_icons += [matcher["add_icon"]]
|
||||
extra_icon_ids += [matcher["add_icon"]]
|
||||
for key in matcher["tags"].keys():
|
||||
processed.add(key)
|
||||
if "color" in matcher:
|
||||
|
@ -167,26 +170,37 @@ class Scheme:
|
|||
processed.add(key)
|
||||
|
||||
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")):
|
||||
fill = self.get_color(tags[tag_key])
|
||||
processed.add(tag_key)
|
||||
|
||||
if main_icon:
|
||||
result_set: List[List[str]] = [main_icon] + extra_icons
|
||||
else:
|
||||
result_set: List[List[str]] = extra_icons
|
||||
for tag_key in tags: # type: str
|
||||
if tag_key in ["color", "colour"]:
|
||||
fill = 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()))
|
||||
|
||||
is_default: bool = False
|
||||
if not result_set and keys_left:
|
||||
result_set = [[DEFAULT_SHAPE_ID]]
|
||||
if not main_icon_id and not extra_icon_ids and keys_left:
|
||||
main_icon_id = [DEFAULT_SHAPE_ID]
|
||||
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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue