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
*.png
*.pyc
*.svg
doc/*.html
doc/*.svg
doc/*.wiki
missed_tags.yml
# 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.
"""
def __init__(
self, check_level, mode: str, seed: str, map_: Map,
flinger: Flinger, scheme: Scheme, icon_extractor: IconExtractor):
self, map_: Map, flinger: Flinger, scheme: Scheme,
icon_extractor: IconExtractor, check_level=lambda x: True,
mode: str = "normal", seed: str = ""):
self.check_level = check_level
self.mode: str = mode
@ -273,6 +274,14 @@ class Constructor:
self.buildings.append(building)
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:
"""
Construct Röntgen ways.

View file

@ -7,16 +7,22 @@ import numpy as np
from colour import Color
from svgwrite import Drawing
from typing import List, Dict, Any, Set
from os.path import join
from roentgen.icon import Icon, IconExtractor
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.
: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 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 " +
", ".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(
file_name: str, combined_icon_ids: List[Set[str]],
extractor: IconExtractor, columns: int = 16, step: float = 24,
color=Color("#444444")):
extractor: IconExtractor, output_directory: str, columns: int = 16,
step: float = 24, color=Color("#444444")
) -> List[List[Icon]]:
"""
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 extractor: icon extractor that generates icon SVG path commands using
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 step: horizontal and vertical distance between icons in grid
:return:
:param color: icon foreground color
"""
point: np.array = np.array((step / 2, step / 2))
width: float = step * columns
@ -92,14 +103,21 @@ def draw_grid(
for icons_to_draw in combined_icon_ids: # type: Set[str]
found: bool = False
icon_set: List[Icon] = []
names = []
for icon_id in icons_to_draw: # 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)
found = True
if icon.name:
names.append(icon.name)
if found:
icons.append(icon_set)
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)
svg: Drawing = Drawing(file_name, (width, height))
@ -124,3 +142,26 @@ def draw_grid(
with open(file_name, "w") as 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).
"""
import re
import xml.dom.minidom
from dataclasses import dataclass
from typing import Dict, Any, Set
from xml.dom.minidom import Document, Element, Node
from typing import Dict, Any, Optional
from xml.dom.minidom import Document, Element, Node, parse
import numpy as np
import svgwrite
@ -31,6 +30,7 @@ class Icon:
path: str # SVG icon path
offset: np.array # vector that should be used to shift the path
id_: str # shape identifier
name: Optional[str] = None # icon description
def is_default(self) -> bool:
"""
@ -96,7 +96,7 @@ class IconExtractor:
self.icons: Dict[str, Icon] = {}
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
if element.nodeName != "svg":
continue
@ -116,14 +116,16 @@ class IconExtractor:
self.parse(sub_node)
return
if ("id" in node.attributes.keys() and
"d" in node.attributes.keys() and
node.attributes["id"].value):
path: str = node.attributes["d"].value
if (node.hasAttribute("id") and node.hasAttribute("d") and
node.getAttribute("id")):
path: str = node.getAttribute("d")
matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path)
if not matcher:
return
name: Optional[str] = None
def get_offset(value: float):
""" Get negated icon offset from the origin. """
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(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_)
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):
"""

View file

@ -43,14 +43,13 @@ class Painter:
"""
def __init__(
self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
mode: str, draw_captions: str, map_: Map, flinger: Flinger,
self, map_: Map, flinger: Flinger,
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.overlap: int = overlap
self.draw_nodes: bool = draw_nodes
self.mode: str = mode
self.draw_captions: str = draw_captions
@ -60,10 +59,17 @@ class Painter:
self.icon_extractor = icon_extractor
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):
"""
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_length: int = len(ways)
for index, way in enumerate(ways): # type: Figure
@ -179,19 +185,16 @@ 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_radius: float = (25)
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)
direction_color: Color = Color("red")
else:
direction = node.get_tag("direction")
direction_radius: float = (
50 * self.flinger.get_scale(node.coordinates))
direction_radius: float = (50)
direction_color: Color = (
self.scheme.get_color("direction_view_color"))
is_revert_gradient = True
@ -238,15 +241,11 @@ class Painter:
continue
ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes")
node.draw_shapes(self.svg, occupied)
ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")
if self.draw_captions == "no":
return
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] and
self.draw_captions != "no"):
node.draw_texts(
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):
@ -295,7 +294,8 @@ def main(argv) -> None:
"""
if len(argv) == 2:
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
options: argparse.Namespace = ui.parse_options(argv)
@ -303,10 +303,6 @@ def main(argv) -> None:
if not options:
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:
input_file_name = options.input_file_name
else:
@ -325,8 +321,7 @@ def main(argv) -> None:
max1 = np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_))
else:
boundary_box = list(map(
lambda x: float(x.replace('m', '-')), options.boundary_box.split(',')))
boundary_box = list(map(float, options.boundary_box.split(',')))
full = False # Full keys getting
@ -340,9 +335,7 @@ def main(argv) -> None:
print("Fatal: no such file: " + file_name + ".")
sys.exit(1)
osm_reader.parse_osm_file(
file_name, parse_ways=options.draw_ways,
parse_relations=options.draw_ways, full=full)
osm_reader.parse_osm_file(file_name, full=full)
map_: Map = osm_reader.map_
@ -354,7 +347,6 @@ def main(argv) -> None:
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)
@ -375,18 +367,13 @@ 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,
icon_extractor)
if options.draw_ways:
constructor.construct_ways()
constructor.construct_relations()
if options.mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
constructor.construct_nodes()
map_, flinger, scheme, icon_extractor, check_level, options.mode,
options.seed)
constructor.construct()
painter: Painter = Painter(
show_missing_tags=options.show_missing_tags, overlap=options.overlap,
draw_nodes=options.draw_nodes, mode=options.mode,
draw_captions=options.draw_captions,
mode=options.mode, draw_captions=options.draw_captions,
map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor,
scheme=scheme)

View file

@ -77,6 +77,7 @@ class OSMNode(Tagged):
self.id_ = structure["id"]
self.coordinates = np.array((structure["lat"], structure["lon"]))
if "tags" in structure:
self.tags = structure["tags"]
return self
@ -119,6 +120,16 @@ class OSMWay(Tagged):
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:
"""
Is way a cycle way or an area boundary.
@ -171,16 +182,37 @@ class OSMRelation(Tagged):
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:
"""
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.ref: int = int(get_value("ref", text))
self.role: str = get_value("role", text)
return self
def get_value(key: str, text: str):
"""
@ -239,14 +271,29 @@ class OverpassReader:
def __init__(self):
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:
structure = json.load(input_file)
node_map = {}
way_map = {}
for element in structure["elements"]:
if element["type"] == "node":
node = OSMNode().parse_from_structure(element)
node_map[node.id_] = 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:
@ -334,7 +381,7 @@ class OSMReader:
element.nodes.append(
self.map_.node_map[int(get_value("ref", line))])
elif line.startswith("<member"):
element.members.append(OSMMember(line))
element.members.append(OSMMember().parse_from_xml(line))
progress_bar(-1, lines_number, text="Parsing")

View file

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

View file

@ -41,16 +41,6 @@ def parse_options(args) -> argparse.Namespace:
default=18,
dest="scale",
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(
"--captions", "--no-draw-captions",
dest="draw_captions",
@ -86,6 +76,26 @@ def parse_options(args) -> argparse.Namespace:
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(
number: int, total: int, length: int = 20, step: int = 1000,
text: str = "") -> None:

View file

@ -33,3 +33,6 @@ class MinMax:
Get middle point between minimum and maximum.
"""
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).
"""
from os import makedirs
from roentgen.grid import draw_all_icons
def test_icons() -> None:
""" Test grid drawing. """
draw_all_icons("temp.svg")
makedirs("icon_set", exist_ok=True)
draw_all_icons("temp.svg", "icon_set")