diff --git a/requirements.txt b/requirements.txt index 40987c5..763f986 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ +svgwrite +numpy>=1.18.1 PyYAML>=4.2b1 urllib3>=1.25.6 diff --git a/roentgen/grid.py b/roentgen/grid.py index 9583062..2bfac0b 100644 --- a/roentgen/grid.py +++ b/roentgen/grid.py @@ -4,22 +4,21 @@ Author: Sergey Vartanov (me@enzet.ru). import os import random +import svgwrite import yaml from roentgen import extract_icon -from roentgen import svg from typing import Any, Dict def draw_icon( - output_file, icon: Dict[str, Any], x: float, y: float, + svg, icon: Dict[str, Any], x: float, y: float, color: str = "444444"): - output_file.write( - f'\n') + svg.add(svg.path( + d=icon["path"], fill=f"#{color}", stroke="none", + transform=f'translate({icon["x"] + x},{icon["y"] + y})')) def draw_grid(): @@ -103,16 +102,16 @@ def draw_grid(): height = int(number / (width / step) + 1) * step - output_file = svg.SVG(open(icon_grid_file_name, "w+")) - output_file.begin(width, height) + svg = svgwrite.Drawing(icon_grid_file_name, (width, height)) - output_file.rect(0, 0, width, height, color="FFFFFF") + svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF")) for icon in icons: background_color, foreground_color = random.choice(icon_colors) - output_file.rect(x - 2 - 8, y - 2 - 8, 20, 20, color=background_color) + svg.add(svg.rect( + (x - 2 - 8, y - 2 - 8), (20, 20), fill=f"#{background_color}")) for i in icon["icons"]: - draw_icon(output_file, i, x, y, foreground_color) + draw_icon(svg, i, x, y, foreground_color) x += step if x > width - 8: x = step / 2 @@ -121,4 +120,4 @@ def draw_grid(): print(f"Icons: {number}.") - output_file.end() + svg.write(open(icon_grid_file_name, "w")) diff --git a/roentgen/mapper.py b/roentgen/mapper.py index 28db619..9309219 100644 --- a/roentgen/mapper.py +++ b/roentgen/mapper.py @@ -3,23 +3,25 @@ Simple OpenStreetMap renderer. Author: Sergey Vartanov (me@enzet.ru). """ +import numpy as np import os -import random +import svgwrite import sys import yaml -import numpy as np +from svgwrite.container import Group +from svgwrite.path import Path +from svgwrite.shapes import Circle, Rect +from svgwrite.text import Text +from typing import List from roentgen import extract_icon from roentgen import ui -from roentgen import svg from roentgen.constructor import Constructor, get_path from roentgen.flinger import GeoFlinger, Geo -from roentgen.osm_reader import OSMReader -from roentgen.osm_getter import get_osm from roentgen.grid import draw_grid - -from typing import List +from roentgen.osm_getter import get_osm +from roentgen.osm_reader import Map, OSMReader ICONS_FILE_NAME: str = "icons/icons.svg" TAGS_FILE_NAME: str = "data/tags.yml" @@ -31,7 +33,7 @@ class Painter: def __init__( self, show_missing_tags, overlap, draw_nodes, mode, draw_captions, - map_, flinger, output_file, icons, scheme): + map_, flinger, svg: svgwrite.Drawing, icons, scheme): self.show_missing_tags = show_missing_tags self.overlap = overlap @@ -41,16 +43,10 @@ class Painter: self.map_ = map_ self.flinger = flinger - self.output_file = output_file + self.svg: svgwrite.Drawing = svg self.icons = icons self.scheme = scheme - def draw_raw_nodes(self): - for node_id in self.map_.node_map: - node = self.map_.node_map[node_id] - flung = self.flinger.fling(node) - self.output_file.circle(flung[0], flung[1], 0.2, color="FFFFFF") - def no_draw(self, key): if key in self.scheme["tags_to_write"] or \ key in self.scheme["tags_to_skip"]: @@ -125,27 +121,21 @@ class Painter: #------# ###### """ - text = text.replace("&", "and") if out_fill_2: - self.output_file.write( - f'' + text + "") + self.svg.add(Text( + text, (x, y), font_size=size, text_anchor="middle", + font_family="Roboto", fill=f"#{out_fill_2}", + stroke_linejoin="round", stroke_width=5, + stroke=f"#{out_fill_2}", opacity=out_opacity_2)) if out_fill: - self.output_file.write( - f'' + text + "") - self.output_file.write( - f'' + text + "") + self.svg.add(Text( + text, (x, y), font_size=size, text_anchor="middle", + font_family="Roboto", fill=f"#{out_fill}", + stroke_linejoin="round", stroke_width=3, + stroke=f"#{out_fill}", opacity=out_opacity)) + self.svg.add(Text( + text, (x, y), font_size=size, text_anchor="middle", + font_family="Roboto", fill=f"#{fill}")) def wr(self, text, x, y, fill, text_y, size=10): text = text[:26] + ("..." if len(text) > 26 else "") @@ -268,16 +258,12 @@ class Painter: node_2 = self.map_.node_map[way.nodes[i + 1]] flung_1 = self.flinger.fling(Geo(node_1.lat, node_1.lon)) flung_2 = self.flinger.fling(Geo(node_2.lat, node_2.lon)) - shifted_1 = np.add(flung_1, shift_1) - shifted_2 = np.add(flung_2, shift_2) - self.output_file.write( - f'\n') + + self.svg.add(self.svg.path( + d=("M", np.add(flung_1, shift_1), "L", + np.add(flung_2, shift_1), np.add(flung_2, shift_2), + np.add(flung_1, shift_2), "Z"), + fill=f"#{color}", stroke=f"#{color}", stroke_width=1)) elif way.path: # TODO: implement pass @@ -289,34 +275,31 @@ class Painter: if way.kind == "way": if way.nodes: path = get_path(way.nodes, [0, 0], self.map_, self.flinger) - self.output_file.write( - f'\n') + self.svg.add(Path(d=path, style=way.style)) else: - self.output_file.write( - f'\n') + self.svg.add(Path(d=way.path, style=way.style)) # Building shade - self.output_file.write('\n') + building_shade = Group(opacity=0.1) + for way in ways: - if way.kind == "building": - if way.nodes: - shift = [-5, 5] - if way.levels: - shift = [-5 * way.levels, 5 * way.levels] - for i in range(len(way.nodes) - 1): - node_1 = self.map_.node_map[way.nodes[i]] - node_2 = self.map_.node_map[way.nodes[i + 1]] - flung_1 = self.flinger.fling(Geo(node_1.lat, node_1.lon)) - flung_2 = self.flinger.fling(Geo(node_2.lat, node_2.lon)) - self.output_file.write( - f'\n') - self.output_file.write("\n") + if way.kind != "building" or not way.nodes: + continue + shift = [-5, 5] + if way.levels: + shift = [-5 * way.levels, 5 * way.levels] + for i in range(len(way.nodes) - 1): + node_1 = self.map_.node_map[way.nodes[i]] + node_2 = self.map_.node_map[way.nodes[i + 1]] + flung_1 = self.flinger.fling(Geo(node_1.lat, node_1.lon)) + flung_2 = self.flinger.fling(Geo(node_2.lat, node_2.lon)) + building_shade.add(Path( + ("M", flung_1, "L", flung_2, np.add(flung_2, shift), + np.add(flung_1, shift), "Z"), + fill="#000000", stroke="#000000", stroke_width=1)) + + self.svg.add(building_shade) # Building walls @@ -327,18 +310,16 @@ class Painter: # Building roof for way in ways: - if way.kind == "building": - if way.nodes: - shift = [0, -3] - if way.levels: - shift = [0 * way.levels, min(-3, -1 * way.levels)] - path = get_path(way.nodes, shift, self.map_, self.flinger) - self.output_file.write( - f'\n') - else: - self.output_file.write( - f'\n') + if way.kind != "building": + continue + if way.nodes: + shift = [0, -3] + if way.levels: + shift = [0 * way.levels, min(-3, -1 * way.levels)] + path = get_path(way.nodes, shift, self.map_, self.flinger) + self.svg.add(Path(d=path, style=way.style, opacity=1)) + else: + self.svg.add(Path(d=way.path, style=way.style, opacity=1)) # Trees @@ -347,11 +328,10 @@ class Painter: node.tags["natural"] == "tree" and "diameter_crown" in node.tags): continue - self.output_file.circle( - float(node.x) + (random.random() - 0.5) * 10, - float(node.y) + (random.random() - 0.5) * 10, + self.svg.add(Circle( + (float(node.x), float(node.y)), float(node.tags["diameter_crown"]) * 1.2, - color='688C44', fill='688C44', opacity=0.3) + fill="#688C44", stroke="#688C44", opacity=0.3)) # All other nodes @@ -386,20 +366,15 @@ class Painter: def draw_point(self, shape, x, y, fill, size=16, xx=0, yy=0, tags=None): x = int(float(x)) y = int(float(y)) - self.output_file.write( - '') - if tags: - self.output_file.write("") - self.output_file.write( - "\n".join(map(lambda x: x + ": " + tags[x], tags))) - self.output_file.write("") - self.output_file.write("\n") + path = self.svg.path( + d=shape, fill=f"#{fill}", fill_opacity=1, + transform=f"translate({x - size / 2.0 - xx * 16}," + f"{y - size / 2.0 - yy * 16})") + path.set_desc(title="\n".join(map(lambda x: x + ": " + tags[x], tags))) + self.svg.add(path) - def draw_point_outline(self, shape, x, y, fill, mode="default", size=16, xx=0, yy=0): + def draw_point_outline( + self, shape, x, y, fill, mode="default", size=16, xx=0, yy=0): x = int(float(x)) y = int(float(y)) opacity = 0.5 @@ -413,12 +388,12 @@ class Painter: if Y > 200: outline_fill = "000000" opacity = 0.7 - self.output_file.write( - '\n') + self.svg.add(self.svg.path( + d=shape, fill=f"#{outline_fill}", opacity=opacity, + stroke=f"#{outline_fill}", stroke_width=stroke_width, + stroke_linejoin="round", + transform=f"translate({x - size / 2.0 - xx * 16}," + f"{y - size / 2.0 - yy * 16})")) def check_level_number(tags, level): @@ -453,9 +428,9 @@ def main(): if not options: sys.exit(1) - background_color = "EEEEEE" + background_color = "#EEEEEE" if options.mode in ["user-coloring", "time"]: - background_color = "111111" + background_color = "#111111" if options.input_file_name: input_file_name = options.input_file_name @@ -480,18 +455,18 @@ def main(): print("Fatal: no such file: " + file_name + ".") sys.exit(1) - map_ = osm_reader.parse_osm_file( + osm_reader.parse_osm_file( file_name, parse_ways=options.draw_ways, parse_relations=options.draw_ways, full=full) - output_file = svg.SVG(open(options.output_file_name, "w+")) + map_: Map = osm_reader.map_ w, h = list(map(lambda x: float(x), options.size.split(","))) - output_file.begin(w, h) - output_file.write( - "Rӧntgen\n") - output_file.rect(0, 0, w, h, color=background_color) + svg: svgwrite.Drawing = \ + svgwrite.Drawing(options.output_file_name, size=(w, h)) + + svg.add(Rect((0, 0), (w, h), fill=background_color)) min1 = Geo(boundary_box[1], boundary_box[0]) max1 = Geo(boundary_box[3], boundary_box[2]) @@ -537,18 +512,18 @@ def main(): show_missing_tags=options.show_missing_tags, overlap=options.overlap, draw_nodes=options.draw_nodes, mode=options.mode, draw_captions=options.draw_captions, - map_=map_, flinger=flinger, output_file=output_file, icons=icons, + map_=map_, flinger=flinger, svg=svg, icons=icons, scheme=scheme) painter.draw(constructor.nodes, constructor.ways, points) if flinger.space[0] == 0: - output_file.rect(0, 0, w, flinger.space[1], color="FFFFFF") - output_file.rect( - 0, h - flinger.space[1], w, flinger.space[1], color="FFFFFF") + svg.add(Rect((0, 0), (w, flinger.space[1]), fill="#FFFFFF")) + svg.add(Rect( + (0, h - flinger.space[1]), (w, flinger.space[1]), fill="#FFFFFF")) if flinger.space[1] == 0: - output_file.rect(0, 0, flinger.space[0], h, color="FFFFFF") - output_file.rect( - w - flinger.space[0], 0, flinger.space[0], h, color="FFFFFF") + svg.add(Rect((0, 0), (flinger.space[0], h), fill="#FFFFFF")) + svg.add(Rect( + (w - flinger.space[0], 0), (flinger.space[0], h), fill="#FFFFFF")) if options.show_index: print(min1.lon, max1.lon) @@ -595,12 +570,13 @@ def main(): t2 = flinger.fling(Geo( min1.lat + (i + 1) * lat_step, min1.lon + (j + 1) * lon_step)) - output_file.text( - ((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40, - str(int(matrix[i][j])), size=80, color="440000", - opacity=0.1, align="center") + svg.add(Text( + str(int(matrix[i][j])), + (((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40), + font_size=80, fill="440000", + opacity=0.1, align="center")) - output_file.end() + svg.write(open(options.output_file_name, "w")) top_missing_tags = \ sorted(missing_tags.keys(), key=lambda x: -missing_tags[x]) diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py index fd4e2f6..138a5de 100644 --- a/roentgen/osm_reader.py +++ b/roentgen/osm_reader.py @@ -3,7 +3,7 @@ Reading OpenStreetMap data from XML file. Author: Sergey Vartanov """ -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from roentgen import ui diff --git a/roentgen/svg.py b/roentgen/svg.py deleted file mode 100644 index 7de4957..0000000 --- a/roentgen/svg.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -Very simple SVG library. - -Author: Sergey Vartanov (me@enzet.ru) -""" - -import math - - -class SVG: - def __init__(self, file_): - self.file = file_ - self.index = 0 - - def begin(self, width, height): - self.file.write( - '\n\n') - self.file.write( - f'''\n''') - - def end(self): - self.file.write('\n') - - def path( - self, path: str, color: str = "black", width: float = 1, - fill: str = "none", end: str = "butt", id: str = None, - color2: str = None, gx1: float = 0, gy1: float = 0, gx2: float = 0, - gy2: float = 0, dash: str = None, dashoffset: str = None, - opacity: float = 1, transform: str = None): - - if color2: - self.index += 1 - self.file.write( - f'' - f'' - f'\n') - - self.file.write(' \n') - - def line( - self, x1, y1, x2, y2, width=1, color='black', end='butt', id_=None, - color2=None, gx1=None, gy1=None, gx2=None, gy2=None, dash=None, - dashoffset=None, opacity=None): - - if color2: - if not gx1: - gx1 = x1 - if not gy1: - gy1 = y1 - if not gx2: - gx2 = x2 - if not gy2: - gy2 = y2 - self.index += 1 - self.file.write( - f'\n' - f'' - f'' - f'\n') - self.file.write( - f' \n') - - def rect( - self, x, y, width, height, color='black', id=None, rx=0, ry=0, - opacity=1.0, stroke_color='none', stroke_width=1.0): - - self.file.write(' \n') - - def curve(self, x1, y1, x2, y2, x3, y3, x4, y4, id=None, width=1, color='black'): - self.file.write(' \n') - - def rhombus(self, x, y, width, height, color='black', id=None): - self.file.write(''' \n') - - def circle(self, x, y, d, color='black', color2='white', fill='none', - opacity=None, width=1, id_=None, gx=0, gy=0, gr=0, fx=0, fy=0): - is_grad = gx != 0 or gy != 0 or gr != 0 - if is_grad: - self.index += 1 - self.file.write( - f'' - f'' - f'\n') - c = 0.577 - self.file.write( - f' \n') - - def text(self, x, y, text, font='Myriad Pro', size='10', align='left', - color='black', id=None, weight=None, letter_spacing=None, angle=None, - opacity=None): - """ - Drawing SVG element. - """ - if angle is None: - self.file.write(f' 0: - trans = 'transform = "matrix(' + str(math.sin(angle)) + ',' + str(math.cos(angle)) + ',' + \ - str(-math.cos(angle)) + ',' + str(math.sin(angle)) + ',' + str(x) + ',' + str(y) + ')"' - else: - trans = 'transform = "matrix(' + str(math.sin(angle + math.pi)) + ',' + str(math.cos(angle + math.pi)) + ',' + \ - str(-math.cos(angle + math.pi)) + ',' + str(math.sin(angle + math.pi)) + ',' + str(x) + ',' + str(y) + ')"' - self.file.write(' ' + trans) - self.file.write('>') - self.file.write(text) - self.file.write('\n') - - @staticmethod - def get_color(color): - if color == 'none': - return 'none' - if color == 'black': - return 'black' - return '#' + str(color) - - def begin_layer(self, name): - self.file.write(f'\n') - - def end_layer(self): - self.file.write('\n') - - def write(self, raw_code): - self.file.write(raw_code) diff --git a/roentgen/ui.py b/roentgen/ui.py index cca3265..daef08e 100644 --- a/roentgen/ui.py +++ b/roentgen/ui.py @@ -8,7 +8,9 @@ from typing import Optional def parse_options(args): - + """ + Parse Röntgen command-line options. + """ parser = argparse.ArgumentParser() parser.add_argument( @@ -56,9 +58,9 @@ def parse_options(args): "--show-index", dest="show_index", action="store_true") parser.add_argument( "--no-show-index", dest="show_index", action="store_false") - parser.add_argument("--mode", dest="mode", default="normal") - parser.add_argument("--seed", dest="seed", default="") - parser.add_argument("--level", dest="level", default=None) + parser.add_argument("--mode", default="normal") + parser.add_argument("--seed", default="") + parser.add_argument("--level", default=None) arguments = parser.parse_args(args[1:])