Add individual icon drawing; refactor.
5
.gitignore
vendored
|
@ -10,9 +10,10 @@
|
|||
|
||||
# Generated files
|
||||
|
||||
*.png
|
||||
*.pyc
|
||||
*.svg
|
||||
doc/*.html
|
||||
doc/*.svg
|
||||
doc/*.wiki
|
||||
missed_tags.yml
|
||||
|
||||
# Test scheme files
|
||||
|
|
BIN
doc/grid.png
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 144 KiB |
BIN
doc/time.png
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 424 KiB |
BIN
doc/trees.png
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 187 KiB |
BIN
doc/user.png
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 431 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 68 KiB |
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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_}"
|
||||
|
|
|
@ -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")
|
||||
|
|