Issue #20: fix compass points parsing; refactor.

This commit is contained in:
Sergey Vartanov 2020-09-09 23:54:15 +03:00
parent b443a59c49
commit 6d938a1248
13 changed files with 193 additions and 166 deletions

View file

@ -1,4 +1,4 @@
language: python
install:
- pip install -r requirements.txt
script: pytest -v test.py
script: pytest -v

View file

@ -1,4 +1,5 @@
svgwrite
numpy>=1.18.1
PyYAML>=4.2b1
portolan
pyyaml>=4.2b1
svgwrite
urllib3>=1.25.6

View file

@ -1,8 +1,7 @@
import numpy as np
from hashlib import sha256
from datetime import datetime
from hashlib import sha256
from typing import Any, Dict, List, Optional, Set
from roentgen import ui
@ -30,6 +29,11 @@ class Node:
self.layer = 0
self.is_for_node = is_for_node
def get_tag(self, key: str):
if key in self.tags:
return self.tags[key]
return None
class Way:
"""
@ -95,13 +99,12 @@ def get_user_color(text: str, seed: str):
return "#" + "0" * (6 - len(h)) + h
def get_time_color(time):
def get_time_color(time: datetime):
"""
Generate color based on time.
"""
if not time:
return "#000000"
time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
delta = (datetime.now() - time).total_seconds()
time_color = hex(0xFF - min(0xFF, int(delta / 500000.)))[2:]
i_time_color = hex(min(0xFF, int(delta / 500000.)))[2:]

View file

@ -3,42 +3,51 @@ Direction tag support.
Author: Sergey Vartanov (me@enzet.ru).
"""
import math
from typing import Iterator, List, Optional, Union
import numpy as np
from typing import Dict, List, Optional, Iterator
DIRECTIONS: Dict[str, np.array] = {
"N": np.array((0, -1)),
"E": np.array((1, 0)),
"W": np.array((-1, 0)),
"S": np.array((0, 1)),
}
from portolan import middle
def parse_vector(text: str) -> np.array:
"""
Parse vector from text representation: letters N, E, W, S or 360-degree
notation. E.g. NW, 270.
Parse vector from text representation: compass points or 360-degree
notation. E.g. "NW", "270".
:param text: vector text representation
:return: parsed normalized vector
"""
def degree_to_radian(degree: float):
""" Convert value in degrees to radians. """
return degree / 180 * np.pi - np.pi / 2
try:
degree: float = float(text) / 180 * math.pi - math.pi / 2
return np.array((math.cos(degree), math.sin(degree)))
except ValueError as e:
vector: np.array = np.array((0, 0))
for char in text: # type: str
if char not in DIRECTIONS:
return None
vector += DIRECTIONS[char]
return vector / np.linalg.norm(vector)
radians: float = degree_to_radian(float(text))
return np.array((np.cos(radians), np.sin(radians)))
except ValueError:
radians: float = degree_to_radian(middle(text))
return np.array((np.cos(radians), np.sin(radians)))
def rotation_matrix(angle):
"""
Get a matrix to rotate 2D vector by the angle.
:param angle: angle in radians
"""
return np.array([
[np.cos(angle), np.sin(angle)],
[-np.sin(angle), np.cos(angle)]])
class Sector:
"""
Sector described by two vectors.
"""
def __init__(self, text: str):
"""
:param text: sector text representation. E.g. "70-210", "N-NW"
"""
self.start: Optional[np.array]
self.end: Optional[np.array]
@ -47,16 +56,23 @@ class Sector:
self.start = parse_vector(parts[0])
self.end = parse_vector(parts[1])
else:
self.start = parse_vector(text)
self.end = None
vector = parse_vector(text)
angle = np.pi / 12
self.start = np.dot(rotation_matrix(angle), vector)
self.end = np.dot(rotation_matrix(-angle), vector)
def draw(self, center: np.array, radius: float) -> List:
def draw(self, center: np.array, radius: float) \
-> Optional[List[Union[float, str, np.array]]]:
"""
Construct SVG "d" for arc element.
Construct SVG path commands for arc element.
:param center: arc center
:param center: arc center point
:param radius: arc radius
:return: SVG path commands
"""
if self.start is None or self.end is None:
return None
start: np.array = center + radius * self.end
end: np.array = center + radius * self.start
@ -79,5 +95,14 @@ class DirectionSet:
def __str__(self):
return ", ".join(map(str, self.sectors))
def draw(self, center: np.array, radius: float) -> Iterator[str]:
return map(lambda x: x.draw(center, radius), self.sectors)
def draw(self, center: np.array, radius: float) -> Iterator[List]:
"""
Construct SVG "d" for arc elements.
:param center: center point of all arcs
:param radius: radius of all arcs
:return: list of "d" values
"""
return filter(
lambda x: x is not None,
map(lambda x: x.draw(center, radius), self.sectors))

View file

@ -5,6 +5,8 @@ import math
import numpy as np
from typing import Optional
from roentgen.util import MinMax
def get_ratio(maximum, minimum, ratio: float = 1):
return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1])

View file

@ -1,10 +1,10 @@
"""
Icon grid drawing.
Author: Sergey Vartanov (me@enzet.ru).
"""
import numpy as np
import os
import random
import svgwrite
from svgwrite import Drawing
import yaml
from roentgen.extract_icon import Icon, IconExtractor
@ -12,32 +12,21 @@ from roentgen.extract_icon import Icon, IconExtractor
from typing import List
def draw_grid():
def draw_grid(step: float = 24, columns: int = 16):
"""
Draw all possible icon combinations in grid.
:param step: horizontal and vertical distance between icons
:param columns: the number of columns in grid
"""
tags_file_name = "data/tags.yml"
scheme = yaml.load(open(tags_file_name), Loader=yaml.FullLoader)
icons_file_name = "icons/icons.svg"
icon_grid_file_name = "icon_grid.svg"
icon_colors_file_name = "data/icon_colors"
icon_colors = [("FFFFFF", "444444")]
if os.path.isfile(icon_colors_file_name):
icon_colors_file = open(icon_colors_file_name)
for line in icon_colors_file.read().split("\n"):
background_color = \
hex(int(line[0:3]))[2:] + hex(int(line[3:6]))[2:] + \
hex(int(line[6:9]))[2:]
foreground_color = \
hex(int(line[10:13]))[2:] + hex(int(line[13:16]))[2:] + \
hex(int(line[16:19]))[2:]
icon_colors.append((background_color, foreground_color))
step: float = 24
width: float = 24 * 16
width: float = step * columns
point: np.array = np.array((step / 2, step / 2))
to_draw = []
@ -89,20 +78,20 @@ def draw_grid():
icons.append(icon_set)
number += 1
height = int(number / (width / step) + 1) * step
height: int = int(int(number / (width / step) + 1) * step)
svg = svgwrite.Drawing(icon_grid_file_name, (width, height))
svg: Drawing = Drawing(icon_grid_file_name, (width, height))
svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF"))
for icon in icons:
background_color, foreground_color = random.choice(icon_colors)
background_color, foreground_color = "#FFFFFF", "#444444"
svg.add(svg.rect(
point - np.array((-10, -10)), (20, 20),
fill=f"#{background_color}"))
fill=background_color))
for i in icon: # type: Icon
path = i.get_path(svg, point)
path.update({"fill": f"#{foreground_color}"})
path.update({"fill": foreground_color})
svg.add(path)
point += np.array((step, 0))
if point[0] > width - 8:
@ -112,4 +101,5 @@ def draw_grid():
print(f"Icons: {number}.")
svg.write(open(icon_grid_file_name, "w"))
with open(icon_grid_file_name, "w") as output_file:
svg.write(output_file)

View file

@ -15,6 +15,7 @@ from svgwrite.text import Text
from typing import Dict, List
from roentgen import ui
from roentgen.address import get_address
from roentgen.constructor import Constructor, get_path, Node, Way
from roentgen.flinger import GeoFlinger, Geo
from roentgen.grid import draw_grid
@ -39,7 +40,7 @@ class Painter:
"""
def __init__(
self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
mode: str, draw_captions: bool, map_: Map, flinger: GeoFlinger,
mode: str, draw_captions: str, map_: Map, flinger: GeoFlinger,
svg: svgwrite.Drawing, icon_extractor: IconExtractor,
scheme: Scheme):
@ -47,7 +48,7 @@ class Painter:
self.overlap: int = overlap
self.draw_nodes: bool = draw_nodes
self.mode: str = mode
self.draw_captions = draw_captions
self.draw_captions: str = draw_captions
self.map_: Map = map_
self.flinger: GeoFlinger = flinger
@ -150,7 +151,6 @@ class Painter:
Construct labels for not processed tags.
"""
texts = []
address: List[str] = []
name = None
alt_name = None
if "name" in tags:
@ -179,24 +179,9 @@ class Painter:
else:
alt_name = ""
alt_name += "бывш. " + tags["old_name"]
if "addr:postcode" in tags and self.draw_captions != "main":
address.append(tags["addr:postcode"])
tags.pop("addr:postcode", None)
if "addr:country" in tags and self.draw_captions != "main":
address.append(tags["addr:country"])
tags.pop("addr:country", None)
if "addr:city" in tags and self.draw_captions != "main":
address.append(tags["addr:city"])
tags.pop("addr:city", None)
if "addr:street" in tags and self.draw_captions != "main":
street = tags["addr:street"]
if street.startswith("улица "):
street = "ул. " + street[len("улица "):]
address.append(street)
tags.pop("addr:street", None)
if "addr:housenumber" in tags:
address.append(tags["addr:housenumber"])
tags.pop("addr:housenumber", None)
address = get_address(tags, self.draw_captions)
if name:
texts.append({"text": name, "fill": "#000000"})
if alt_name:
@ -338,8 +323,7 @@ class Painter:
# Trees
for node in nodes:
if not("natural" in node.tags and
node.tags["natural"] == "tree" and
if not(node.get_tag("natural") == "tree" and
"diameter_crown" in node.tags):
continue
self.svg.add(Circle(
@ -348,23 +332,27 @@ class Painter:
# Directions
for node in nodes:
if not ("tourism" in node.tags and
node.tags["tourism"] == "viewpoint" and
"direction" in node.tags):
for node in nodes: # type: Node
direction = None
if node.get_tag("tourism") == "viewpoint":
direction = node.get_tag("direction")
if node.get_tag("man_made") == "surveillance":
direction = node.get_tag("camera:direction")
if not direction:
continue
DIRECTION_RADIUS: int = 50
DIRECTION_RADIUS: int = 25
DIRECTION_COLOR: str = self.scheme.get_color("direction_color")
for path in DirectionSet(node.tags["direction"])\
for path in DirectionSet(direction)\
.draw(node.point, DIRECTION_RADIUS):
gradient = self.svg.defs.add(self.svg.radialGradient(
center=node.point, r=DIRECTION_RADIUS,
gradientUnits="userSpaceOnUse"))
gradient\
.add_stop_color(0, DIRECTION_COLOR, opacity=0)\
.add_stop_color(1, DIRECTION_COLOR, opacity=0.7)
.add_stop_color(1, DIRECTION_COLOR, opacity=0.4)
self.svg.add(self.svg.path(
d=["M", node.point] + path + ["L", node.point, "Z"],
fill=gradient.get_paint_server()))
@ -372,12 +360,14 @@ class Painter:
# All other nodes
nodes = sorted(nodes, key=lambda x: x.layer)
for node in nodes: # type: Node
for index, node in enumerate(nodes): # type: int, Node
if "natural" in node.tags and \
node.tags["natural"] == "tree" and \
"diameter_crown" in node.tags:
continue
ui.progress_bar(index, len(nodes), step=10)
self.draw_shapes(node, points)
ui.progress_bar(-1, len(nodes), step=10)
if self.draw_captions == "no":
return
@ -405,7 +395,7 @@ class Painter:
point = np.array(list(map(lambda x: int(x), point)))
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
path = icon.get_path(self.svg, point)
path: svgwrite.path.Path = icon.get_path(self.svg, point)
path.update({"fill": fill})
path.set_desc(title=title)
self.svg.add(path)
@ -518,15 +508,14 @@ def main():
min1 = Geo(boundary_box[1], boundary_box[0])
max1 = Geo(boundary_box[3], boundary_box[2])
authors = {}
missing_tags = {}
points = []
scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME)
scheme: Scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME)
flinger = GeoFlinger(min1, max1, [0, 0], [w, h])
flinger: GeoFlinger = GeoFlinger(min1, max1, [0, 0], [w, h])
icon_extractor = IconExtractor(ICONS_FILE_NAME)
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
def check_level(x):
""" Draw objects on all levels. """
@ -544,14 +533,14 @@ def main():
""" Draw objects on the specified level. """
return not check_level_number(x, float(options.level))
constructor = Constructor(
constructor: Constructor = Constructor(
check_level, options.mode, options.seed, map_, flinger, scheme)
if options.draw_ways:
constructor.construct_ways()
constructor.construct_relations()
constructor.construct_nodes()
painter = Painter(
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,
@ -569,24 +558,35 @@ def main():
(w - flinger.space[0], 0), (flinger.space[0], h), fill="#FFFFFF"))
if options.show_index:
draw_index(flinger, map_, max1, min1, svg)
print("Writing output SVG...")
svg.write(open(options.output_file_name, "w"))
print("Done")
top_missing_tags = \
sorted(missing_tags.keys(), key=lambda x: -missing_tags[x])
missing_tags_file = open(MISSING_TAGS_FILE_NAME, "w+")
for tag in top_missing_tags:
missing_tags_file.write(
f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n')
missing_tags_file.close()
def draw_index(flinger, map_, max1, min1, svg):
print(min1.lon, max1.lon)
print(min1.lat, max1.lat)
lon_step = 0.001
lat_step = 0.001
matrix = []
lat_number = int((max1.lat - min1.lat) / lat_step) + 1
lon_number = int((max1.lon - min1.lon) / lon_step) + 1
for i in range(lat_number):
row = []
for j in range(lon_number):
row.append(0)
matrix.append(row)
for node_id in map_.node_map:
for node_id in map_.node_map: # type: int
node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step)
j = int((node.lon - min1.lon) / lon_step)
@ -594,8 +594,7 @@ def main():
matrix[i][j] += 1
if "tags" in node:
matrix[i][j] += len(node.tags)
for way_id in map_.way_map:
for way_id in map_.way_map: # type: int
way = map_.way_map[way_id]
if "tags" in way:
for node_id in way.nodes:
@ -605,7 +604,6 @@ def main():
if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += len(way.tags) / float(
len(way.nodes))
for i in range(lat_number):
for j in range(lon_number):
t1 = flinger.fling(Geo(
@ -618,17 +616,3 @@ def main():
(((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40),
font_size=80, fill="440000",
opacity=0.1, align="center"))
svg.write(open(options.output_file_name, "w"))
top_missing_tags = \
sorted(missing_tags.keys(), key=lambda x: -missing_tags[x])
missing_tags_file = open(MISSING_TAGS_FILE_NAME, "w+")
for tag in top_missing_tags:
missing_tags_file.write(
f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n')
missing_tags_file.close()
top_authors = sorted(authors.keys(), key=lambda x: -authors[x])
for author in top_authors:
print(f"{author}: {authors[author]}")

View file

@ -3,6 +3,7 @@ Reading OpenStreetMap data from XML file.
Author: Sergey Vartanov
"""
from datetime import datetime
from typing import Dict, List, Optional
from roentgen import ui
@ -61,7 +62,7 @@ class OSMWay:
self.visible: Optional[str] = None
self.changeset: Optional[str] = None
self.user: Optional[str] = None
self.timestamp: Optional[str] = None
self.timestamp: Optional[datetime] = None
self.uid: Optional[str] = None
def parse_from_xml(self, text: str, is_full: bool = False) -> "OSMWay":
@ -76,7 +77,8 @@ class OSMWay:
if is_full:
self.visible = get_value("visible", text)
self.changeset = get_value("changeset", text)
self.timestamp = get_value("timestamp", text)
self.timestamp = datetime.strptime(
get_value("timestamp", text), "%Y-%m-%dT%H:%M:%SZ")
self.user = get_value("user", text)
self.uid = get_value("uid", text)
@ -190,7 +192,6 @@ class OSMReader:
if not parse_nodes:
if parse_ways or parse_relations:
continue
else:
break
if line[-3] == "/":
node: OSMNode = OSMNode().parse_from_xml(line[7:-3], full)
@ -206,7 +207,6 @@ class OSMReader:
if not parse_ways:
if parse_relations:
continue
else:
break
if line[-3] == '/':
way = OSMWay().parse_from_xml(line[6:-3], full)

View file

@ -39,7 +39,8 @@ def parse_options(args):
"-s", "--size",
metavar="<width>,<height>",
help="output SVG file size in pixels",
dest="size")
dest="size",
required=True)
parser.add_argument(
"-nn", "--no-draw-nodes",
dest="draw_nodes",

22
test/test_direction.py Normal file
View file

@ -0,0 +1,22 @@
"""
Test direction processing.
Author: Sergey Vartanov (me@enzet.ru).
"""
import numpy as np
from roentgen.direction import parse_vector
def test_compass_points_1():
assert np.allclose(parse_vector("N"), np.array([0, -1]))
def test_compass_points_2():
root: np.float64 = -np.sqrt(2) / 2
assert np.allclose(parse_vector("NW"), np.array([root, root]))
def test_compass_points_3():
assert np.allclose(
parse_vector("SSW"), np.array([-0.38268343, 0.92387953]))

View file

@ -2,10 +2,6 @@
Author: Sergey Vartanov (me@enzet.ru).
"""
import os
import random
import yaml
from roentgen.flinger import map_
from roentgen.grid import draw_grid

3
test_main.py Normal file
View file

@ -0,0 +1,3 @@
def test_main():
assert True