Add individual icon drawing; refactor.

This commit is contained in:
Sergey Vartanov 2021-04-24 01:56:23 +03:00
parent 81ab4e14c7
commit 813ad6b806
16 changed files with 181 additions and 72 deletions

5
.gitignore vendored
View file

@ -10,9 +10,10 @@
# Generated files # Generated files
*.png
*.pyc *.pyc
*.svg doc/*.html
doc/*.svg
doc/*.wiki
missed_tags.yml missed_tags.yml
# Test scheme files # Test scheme files

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 424 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 187 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 431 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

View file

@ -249,8 +249,9 @@ class Constructor:
Röntgen node and way constructor. Röntgen node and way constructor.
""" """
def __init__( def __init__(
self, check_level, mode: str, seed: str, map_: Map, self, map_: Map, flinger: Flinger, scheme: Scheme,
flinger: Flinger, scheme: Scheme, icon_extractor: IconExtractor): icon_extractor: IconExtractor, check_level=lambda x: True,
mode: str = "normal", seed: str = ""):
self.check_level = check_level self.check_level = check_level
self.mode: str = mode self.mode: str = mode
@ -273,6 +274,14 @@ class Constructor:
self.buildings.append(building) self.buildings.append(building)
self.levels.add(building.get_levels()) self.levels.add(building.get_levels())
def construct(self) -> None:
"""
Construct nodes, ways, and relations.
"""
self.construct_ways()
self.construct_relations()
self.construct_nodes()
def construct_ways(self) -> None: def construct_ways(self) -> None:
""" """
Construct Röntgen ways. Construct Röntgen ways.

View file

@ -7,16 +7,22 @@ import numpy as np
from colour import Color from colour import Color
from svgwrite import Drawing from svgwrite import Drawing
from typing import List, Dict, Any, Set from typing import List, Dict, Any, Set
from os.path import join
from roentgen.icon import Icon, IconExtractor from roentgen.icon import Icon, IconExtractor
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
def draw_all_icons(output_file_name: str, columns: int = 16, step: float = 24): def draw_all_icons(
output_file_name: str, output_directory: str, columns: int = 16,
step: float = 24
) -> None:
""" """
Draw all possible icon combinations in grid. Draw all possible icon combinations in grid.
:param output_file_name: output SVG file name for icon grid :param output_file_name: output SVG file name for icon grid
:param output_directory: path to the directory to store individual SVG files
for icons
:param columns: the number of columns in grid :param columns: the number of columns in grid
:param step: horizontal and vertical distance between icons :param step: horizontal and vertical distance between icons
""" """
@ -66,13 +72,16 @@ def draw_all_icons(output_file_name: str, columns: int = 16, step: float = 24):
"Icons with no tag specification: \n " + "Icons with no tag specification: \n " +
", ".join(sorted(extractor.icons.keys() - specified_ids)) + ".") ", ".join(sorted(extractor.icons.keys() - specified_ids)) + ".")
draw_grid(output_file_name, to_draw, extractor, columns, step) draw_grid(
output_file_name, to_draw, extractor, output_directory, columns, step
)
def draw_grid( def draw_grid(
file_name: str, combined_icon_ids: List[Set[str]], file_name: str, combined_icon_ids: List[Set[str]],
extractor: IconExtractor, columns: int = 16, step: float = 24, extractor: IconExtractor, output_directory: str, columns: int = 16,
color=Color("#444444")): step: float = 24, color=Color("#444444")
) -> List[List[Icon]]:
""" """
Draw icons in the form of table Draw icons in the form of table
@ -80,9 +89,11 @@ def draw_grid(
:param combined_icon_ids: list of set of icon string identifiers :param combined_icon_ids: list of set of icon string identifiers
:param extractor: icon extractor that generates icon SVG path commands using :param extractor: icon extractor that generates icon SVG path commands using
its string identifier its string identifier
:param output_directory: path to the directory to store individual SVG files
for icons
:param columns: number of columns in grid :param columns: number of columns in grid
:param step: horizontal and vertical distance between icons in grid :param step: horizontal and vertical distance between icons in grid
:return: :param color: icon foreground color
""" """
point: np.array = np.array((step / 2, step / 2)) point: np.array = np.array((step / 2, step / 2))
width: float = step * columns width: float = step * columns
@ -92,14 +103,21 @@ def draw_grid(
for icons_to_draw in combined_icon_ids: # type: Set[str] for icons_to_draw in combined_icon_ids: # type: Set[str]
found: bool = False found: bool = False
icon_set: List[Icon] = [] icon_set: List[Icon] = []
names = []
for icon_id in icons_to_draw: # type: str for icon_id in icons_to_draw: # type: str
icon, extracted = extractor.get_path(icon_id) # type: Icon, bool icon, extracted = extractor.get_path(icon_id) # type: Icon, bool
assert extracted, f"no icon with ID {icon_id}" assert extracted, f"no icon with ID {icon_id}"
icon_set.append(icon) icon_set.append(icon)
found = True found = True
if icon.name:
names.append(icon.name)
if found: if found:
icons.append(icon_set) icons.append(icon_set)
number += 1 number += 1
draw_icon(
join(output_directory, f"Röntgen {' + '.join(names)}.svg"),
icons_to_draw, extractor
)
height: int = int(int(number / (width / step) + 1) * step) height: int = int(int(number / (width / step) + 1) * step)
svg: Drawing = Drawing(file_name, (width, height)) svg: Drawing = Drawing(file_name, (width, height))
@ -124,3 +142,26 @@ def draw_grid(
with open(file_name, "w") as output_file: with open(file_name, "w") as output_file:
svg.write(output_file) svg.write(output_file)
return icons
def draw_icon(
file_name: str, icon_ids: Set[str], extractor: IconExtractor
) -> None:
icon_set: List[Icon] = []
for icon_id in icon_ids: # type: str
icon, extracted = extractor.get_path(icon_id) # type: Icon, bool
assert extracted, f"no icon with ID {icon_id}"
icon_set.append(icon)
svg: Drawing = Drawing(file_name, (16, 16))
for icon in icon_set: # type: Icon
path = icon.get_path(svg, (8, 8))
path.update({"fill": "black"})
svg.add(path)
with open(file_name, "w") as output_file:
svg.write(output_file)

View file

@ -4,10 +4,9 @@ Extract icons from SVG file.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
import re import re
import xml.dom.minidom
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Any, Set from typing import Dict, Any, Optional
from xml.dom.minidom import Document, Element, Node from xml.dom.minidom import Document, Element, Node, parse
import numpy as np import numpy as np
import svgwrite import svgwrite
@ -31,6 +30,7 @@ class Icon:
path: str # SVG icon path path: str # SVG icon path
offset: np.array # vector that should be used to shift the path offset: np.array # vector that should be used to shift the path
id_: str # shape identifier id_: str # shape identifier
name: Optional[str] = None # icon description
def is_default(self) -> bool: def is_default(self) -> bool:
""" """
@ -96,7 +96,7 @@ class IconExtractor:
self.icons: Dict[str, Icon] = {} self.icons: Dict[str, Icon] = {}
with open(svg_file_name) as input_file: with open(svg_file_name) as input_file:
content = xml.dom.minidom.parse(input_file) # type: Document content = parse(input_file) # type: Document
for element in content.childNodes: # type: Element for element in content.childNodes: # type: Element
if element.nodeName != "svg": if element.nodeName != "svg":
continue continue
@ -116,14 +116,16 @@ class IconExtractor:
self.parse(sub_node) self.parse(sub_node)
return return
if ("id" in node.attributes.keys() and if (node.hasAttribute("id") and node.hasAttribute("d") and
"d" in node.attributes.keys() and node.getAttribute("id")):
node.attributes["id"].value):
path: str = node.attributes["d"].value path: str = node.getAttribute("d")
matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path) matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path)
if not matcher: if not matcher:
return return
name: Optional[str] = None
def get_offset(value: float): def get_offset(value: float):
""" Get negated icon offset from the origin. """ """ Get negated icon offset from the origin. """
return -int(value / GRID_STEP) * GRID_STEP - GRID_STEP / 2 return -int(value / GRID_STEP) * GRID_STEP - GRID_STEP / 2
@ -132,10 +134,15 @@ class IconExtractor:
get_offset(float(matcher.group(1))), get_offset(float(matcher.group(1))),
get_offset(float(matcher.group(2))))) get_offset(float(matcher.group(2)))))
id_: str = node.attributes["id"].value for child_node in node.childNodes:
if isinstance(child_node, Element):
name = child_node.childNodes[0].nodeValue
break
id_: str = node.getAttribute("id")
matcher = re.match(STANDARD_INKSCAPE_ID, id_) matcher = re.match(STANDARD_INKSCAPE_ID, id_)
if not matcher: if not matcher:
self.icons[id_] = Icon(node.attributes["d"].value, point, id_) self.icons[id_] = Icon(path, point, id_, name)
def get_path(self, id_: str) -> (Icon, bool): def get_path(self, id_: str) -> (Icon, bool):
""" """

View file

@ -43,14 +43,13 @@ class Painter:
""" """
def __init__( def __init__(
self, show_missing_tags: bool, overlap: int, draw_nodes: bool, self, map_: Map, flinger: Flinger,
mode: str, draw_captions: str, map_: Map, flinger: Flinger,
svg: svgwrite.Drawing, icon_extractor: IconExtractor, svg: svgwrite.Drawing, icon_extractor: IconExtractor,
scheme: Scheme): scheme: Scheme, show_missing_tags: bool = False, overlap: int = 12,
mode: str = "normal", draw_captions: str = "main"):
self.show_missing_tags: bool = show_missing_tags self.show_missing_tags: bool = show_missing_tags
self.overlap: int = overlap self.overlap: int = overlap
self.draw_nodes: bool = draw_nodes
self.mode: str = mode self.mode: str = mode
self.draw_captions: str = draw_captions self.draw_captions: str = draw_captions
@ -60,10 +59,17 @@ class Painter:
self.icon_extractor = icon_extractor self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.background_color: Color = self.scheme.get_color("background_color")
if self.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
self.background_color: Color = Color("#111111")
def draw(self, constructor: Constructor): def draw(self, constructor: Constructor):
""" """
Draw map. Draw map.
""" """
self.svg.add(Rect(
(0, 0), self.flinger.size, fill=self.background_color))
ways = sorted(constructor.figures, key=lambda x: x.line_style.priority) ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
ways_length: int = len(ways) ways_length: int = len(ways)
for index, way in enumerate(ways): # type: Figure for index, way in enumerate(ways): # type: Figure
@ -179,19 +185,16 @@ 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)
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)
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)
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
@ -238,15 +241,11 @@ class Painter:
continue continue
ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes") ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes")
node.draw_shapes(self.svg, occupied) node.draw_shapes(self.svg, occupied)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes") if (self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE] and
self.draw_captions != "no"):
if self.draw_captions == "no":
return
for node in nodes: # type: Point
if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
node.draw_texts( node.draw_texts(
self.svg, self.scheme, occupied, self.draw_captions) self.svg, self.scheme, occupied, self.draw_captions)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
def check_level_number(tags: Dict[str, Any], level: float): def check_level_number(tags: Dict[str, Any], level: float):
@ -295,7 +294,8 @@ def main(argv) -> None:
""" """
if len(argv) == 2: if len(argv) == 2:
if argv[1] == "grid": if argv[1] == "grid":
draw_all_icons("icon_grid.svg") os.makedirs("icon_set", exist_ok=True)
draw_all_icons("icon_grid.svg", "icon_set")
return return
options: argparse.Namespace = ui.parse_options(argv) options: argparse.Namespace = ui.parse_options(argv)
@ -303,10 +303,6 @@ def main(argv) -> None:
if not options: if not options:
sys.exit(1) sys.exit(1)
background_color: Color = Color("#EEEEEE")
if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
background_color: Color = Color("#111111")
if options.input_file_name: if options.input_file_name:
input_file_name = options.input_file_name input_file_name = options.input_file_name
else: else:
@ -325,8 +321,7 @@ def main(argv) -> None:
max1 = np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_)) max1 = np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_))
else: else:
boundary_box = list(map( boundary_box = list(map(float, options.boundary_box.split(',')))
lambda x: float(x.replace('m', '-')), options.boundary_box.split(',')))
full = False # Full keys getting full = False # Full keys getting
@ -340,9 +335,7 @@ def main(argv) -> None:
print("Fatal: no such file: " + file_name + ".") print("Fatal: no such file: " + file_name + ".")
sys.exit(1) sys.exit(1)
osm_reader.parse_osm_file( osm_reader.parse_osm_file(file_name, full=full)
file_name, parse_ways=options.draw_ways,
parse_relations=options.draw_ways, full=full)
map_: Map = osm_reader.map_ map_: Map = osm_reader.map_
@ -354,7 +347,6 @@ def main(argv) -> None:
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))
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME) icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
@ -375,18 +367,13 @@ 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, map_, flinger, scheme, icon_extractor, check_level, options.mode,
icon_extractor) options.seed)
if options.draw_ways: constructor.construct()
constructor.construct_ways()
constructor.construct_relations()
if options.mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
constructor.construct_nodes()
painter: Painter = Painter( painter: Painter = Painter(
show_missing_tags=options.show_missing_tags, overlap=options.overlap, show_missing_tags=options.show_missing_tags, overlap=options.overlap,
draw_nodes=options.draw_nodes, mode=options.mode, mode=options.mode, draw_captions=options.draw_captions,
draw_captions=options.draw_captions,
map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor, map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor,
scheme=scheme) scheme=scheme)

View file

@ -77,7 +77,8 @@ class OSMNode(Tagged):
self.id_ = structure["id"] self.id_ = structure["id"]
self.coordinates = np.array((structure["lat"], structure["lon"])) self.coordinates = np.array((structure["lat"], structure["lon"]))
self.tags = structure["tags"] if "tags" in structure:
self.tags = structure["tags"]
return self return self
@ -119,6 +120,16 @@ class OSMWay(Tagged):
return self return self
def parse_from_structure(self, structure: Dict[str, Any], nodes) -> "OSMWay":
self.id_ = structure["id"]
for node_id in structure["nodes"]:
self.nodes.append(nodes[node_id])
if "tags" in structure:
self.tags = structure["tags"]
return self
def is_cycle(self) -> bool: def is_cycle(self) -> bool:
""" """
Is way a cycle way or an area boundary. Is way a cycle way or an area boundary.
@ -171,16 +182,37 @@ class OSMRelation(Tagged):
return self return self
def parse_from_structure(self, structure: Dict[str, Any]) -> "OSMRelation":
self.id_ = structure["id"]
for member in structure["members"]:
mem = OSMMember()
mem.type_ = member["type"]
mem.role = member["role"]
mem.ref = member["ref"]
self.members.append(mem)
if "tags" in structure:
self.tags = structure["tags"]
return self
class OSMMember: class OSMMember:
""" """
Member of OpenStreetMap relation. Member of OpenStreetMap relation.
""" """
def __init__(self, text: str): def __init__(self):
self.type_ = ""
self.ref = 0
self.role = ""
def parse_from_xml(self, text: str) -> "OSMMember":
self.type_: str = get_value("type", text) self.type_: str = get_value("type", text)
self.ref: int = int(get_value("ref", text)) self.ref: int = int(get_value("ref", text))
self.role: str = get_value("role", text) self.role: str = get_value("role", text)
return self
def get_value(key: str, text: str): def get_value(key: str, text: str):
""" """
@ -239,14 +271,29 @@ class OverpassReader:
def __init__(self): def __init__(self):
self.map_ = Map() self.map_ = Map()
def parse_json_file(self, file_name: str): def parse_json_file(self, file_name: str) -> Map:
with open(file_name) as input_file: with open(file_name) as input_file:
structure = json.load(input_file) structure = json.load(input_file)
node_map = {}
way_map = {}
for element in structure["elements"]: for element in structure["elements"]:
if element["type"] == "node": if element["type"] == "node":
node = OSMNode().parse_from_structure(element) node = OSMNode().parse_from_structure(element)
node_map[node.id_] = node
self.map_.add_node(node) self.map_.add_node(node)
for element in structure["elements"]:
if element["type"] == "way":
way = OSMWay().parse_from_structure(element, node_map)
way_map[way.id_] = way
self.map_.add_way(way)
for element in structure["elements"]:
if element["type"] == "relation":
relation = OSMRelation().parse_from_structure(element)
self.map_.add_relation(relation)
return self.map_
class OSMReader: class OSMReader:
@ -334,7 +381,7 @@ class OSMReader:
element.nodes.append( element.nodes.append(
self.map_.node_map[int(get_value("ref", line))]) self.map_.node_map[int(get_value("ref", line))])
elif line.startswith("<member"): elif line.startswith("<member"):
element.members.append(OSMMember(line)) element.members.append(OSMMember().parse_from_xml(line))
progress_bar(-1, lines_number, text="Parsing") progress_bar(-1, lines_number, text="Parsing")

View file

@ -254,13 +254,15 @@ class Scheme:
for key in element: # type: str for key in element: # type: str
if key not in [ if key not in [
"tags", "no_tags", "priority", "level", "icon", "tags", "no_tags", "priority", "level", "icon",
"r", "r2"]: "r", "r1", "r2"]:
value = element[key] value = element[key]
if isinstance(value, str) and value.endswith("_color"): if isinstance(value, str) and value.endswith("_color"):
value = self.get_color(value) value = self.get_color(value)
style[key] = value style[key] = value
if "r" in element: if "r" in element:
style["stroke-width"] = (element["r"] * scale) style["stroke-width"] = (element["r"] * scale)
if "r1" in element:
style["stroke-width"] = (element["r1"] * scale + 1)
if "r2" in element: if "r2" in element:
style["stroke-width"] = (element["r2"] * scale + 2) style["stroke-width"] = (element["r2"] * scale + 2)

View file

@ -41,16 +41,6 @@ def parse_options(args) -> argparse.Namespace:
default=18, default=18,
dest="scale", dest="scale",
type=float) type=float)
parser.add_argument(
"-nn", "--no-draw-nodes",
dest="draw_nodes",
action="store_false",
default=True)
parser.add_argument(
"-nw", "--no-draw-ways",
dest="draw_ways",
action="store_false",
default=True)
parser.add_argument( parser.add_argument(
"--captions", "--no-draw-captions", "--captions", "--no-draw-captions",
dest="draw_captions", dest="draw_captions",
@ -86,6 +76,26 @@ def parse_options(args) -> argparse.Namespace:
return arguments return arguments
def progress_bar1(
number: int, total: int, length: int = 20, step: int = 1000) -> None:
"""
Draw progress bar using Unicode symbols.
:param number: current value
:param total: maximum value
:param length: progress bar length.
:param step: frequency of progress bar updating (assuming that numbers go
subsequently)
:param text: short description
"""
ratio: float = number / total
parts: int = int(ratio * length * BOXES_LENGTH)
fill_length: int = int(parts / BOXES_LENGTH)
box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)]
return (
f"{fill_length * ''}{box}{int(length - fill_length - 1) * ' '}")
def progress_bar( def progress_bar(
number: int, total: int, length: int = 20, step: int = 1000, number: int, total: int, length: int = 20, step: int = 1000,
text: str = "") -> None: text: str = "") -> None:

View file

@ -33,3 +33,6 @@ class MinMax:
Get middle point between minimum and maximum. Get middle point between minimum and maximum.
""" """
return (self.min_ + self.max_) / 2 return (self.min_ + self.max_) / 2
def __repr__(self) -> str:
return f"{self.min_}:{self.max_}"

View file

@ -1,10 +1,12 @@
""" """
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
from os import makedirs
from roentgen.grid import draw_all_icons from roentgen.grid import draw_all_icons
def test_icons() -> None: def test_icons() -> None:
""" Test grid drawing. """ """ Test grid drawing. """
draw_all_icons("temp.svg") makedirs("icon_set", exist_ok=True)
draw_all_icons("temp.svg", "icon_set")