Fix direction; add scale computing.

This commit is contained in:
Sergey Vartanov 2020-09-13 21:52:42 +03:00
parent 197ee5c0ec
commit 8d8080181e
7 changed files with 133 additions and 55 deletions

View file

@ -30,4 +30,3 @@ def get_address(tags: Dict[str, Any], draw_captions_mode: str):
if "addr:housenumber" in tags:
address.append(tags["addr:housenumber"])
tags.pop("addr:housenumber", None)

View file

@ -27,6 +27,8 @@ class Node:
self, icon_set: IconSet, tags: Dict[str, str],
point: (float, float), path: Optional[str],
priority: int = 0, is_for_node: bool = True):
assert point is not None
self.icon_set: IconSet = icon_set
self.tags = tags
self.point = point
@ -80,8 +82,8 @@ def line_center(nodes: List[OSMNode], flinger: GeoFlinger) -> np.array:
for node in nodes: # type: OSMNode
flung = flinger.fling(node.position)
x.add(flung[0])
y.add(flung[1])
x.update(flung[0])
y.update(flung[1])
return np.array(((x.min_ + x.max_) / 2.0, (y.min_ + y.max_) / 2.0))
@ -296,17 +298,19 @@ class Constructor:
style[key] = value
self.ways.append(
Way(kind, nodes, path, style, layer, 50, levels))
if center_point is not None and way.is_cycle() or \
"area" in tags and tags["area"]:
if center_point is not None and \
(way.is_cycle() or "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node(
icon_set, tags, center_point, path, is_for_node=False))
appended = True
if not appended and DEBUG:
style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way(kind, nodes, path, style, layer, 50, levels))
if not appended:
if DEBUG:
style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way(
kind, nodes, path, style, layer, 50, levels))
if center_point is not None and way.is_cycle() or \
"area" in tags and tags["area"]:
icon_set: IconSet = self.scheme.get_icon(tags)

View file

@ -8,8 +8,19 @@ from typing import Iterator, List, Optional, Union
import numpy as np
from portolan import middle
Path = Union[float, str, np.array]
def parse_vector(text: str) -> np.array:
SHIFT: float = -np.pi / 2
SMALLEST_ANGLE: float = np.pi / 15
DEFAULT_ANGLE: float = np.pi / 30
def degree_to_radian(degree: float) -> float:
""" Convert value in degrees to radians. """
return degree / 180 * np.pi
def parse_vector(text: str) -> Optional[np.array]:
"""
Parse vector from text representation: compass points or 360-degree
notation. E.g. "NW", "270".
@ -17,16 +28,19 @@ def parse_vector(text: str) -> np.array:
: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:
radians: float = degree_to_radian(float(text))
radians: float = degree_to_radian(float(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except ValueError:
radians: float = degree_to_radian(middle(text))
pass
try:
radians: float = degree_to_radian(middle(text)) + SHIFT
return np.array((np.cos(radians), np.sin(radians)))
except KeyError:
pass
return None
def rotation_matrix(angle):
@ -44,25 +58,32 @@ class Sector:
"""
Sector described by two vectors.
"""
def __init__(self, text: str):
def __init__(self, text: str, angle: Optional[float] = None):
"""
:param text: sector text representation. E.g. "70-210", "N-NW"
:param angle: angle in degrees
"""
self.start: Optional[np.array]
self.end: Optional[np.array]
self.start: Optional[np.array] = None
self.end: Optional[np.array] = None
if "-" in text:
parts: List[str] = text.split("-")
self.start = parse_vector(parts[0])
self.end = parse_vector(parts[1])
else:
vector = parse_vector(text)
angle = np.pi / 12
self.start = np.dot(rotation_matrix(angle), vector)
self.end = np.dot(rotation_matrix(-angle), vector)
if angle is None:
angle = DEFAULT_ANGLE
else:
angle = degree_to_radian(angle) / 2
angle = max(SMALLEST_ANGLE, angle)
def draw(self, center: np.array, radius: float) \
-> Optional[List[Union[float, str, np.array]]]:
vector: Optional[np.array] = parse_vector(text)
if vector is not None:
self.start = np.dot(rotation_matrix(angle), vector)
self.end = np.dot(rotation_matrix(-angle), vector)
def draw(self, center: np.array, radius: float) -> Optional[List[Path]]:
"""
Construct SVG path commands for arc element.
@ -95,7 +116,7 @@ class DirectionSet:
def __str__(self):
return ", ".join(map(str, self.sectors))
def draw(self, center: np.array, radius: float) -> Iterator[List]:
def draw(self, center: np.array, radius: float) -> Iterator[List[Path]]:
"""
Construct SVG "d" for arc elements.

View file

@ -5,8 +5,6 @@ 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])
@ -48,7 +46,7 @@ class Geo:
class GeoFlinger:
def __init__(
self, minimum, maximum, target_minimum=None, target_maximum=None):
self, minimum, maximum, target_minimum, target_maximum):
"""
:param minimum: minimum latitude and longitude
:param maximum: maximum latitude and longitude
@ -71,7 +69,7 @@ class GeoFlinger:
# Ratio is x / y.
space = [0, 0]
space: np.array = [0, 0]
current_ratio = get_ratio(self.maximum, self.minimum, ratio)
target_ratio = get_ratio(target_maximum, target_minimum)
@ -93,6 +91,13 @@ class GeoFlinger:
self.target_minimum = np.add(target_minimum, space)
self.target_maximum = np.subtract(target_maximum, space)
meters_per_pixel = \
(self.maximum.lat - self.minimum.lat) / \
(self.target_maximum[1] - self.target_minimum[1]) * \
40000 / 360 * 1000
self.scale = 1 / meters_per_pixel
self.space = space
def fling(self, current) -> np.array:

View file

@ -10,7 +10,7 @@ import sys
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.shapes import Circle, Rect
from svgwrite.shapes import Rect
from svgwrite.text import Text
from typing import Any, Dict, List
@ -23,7 +23,7 @@ from roentgen.extract_icon import Icon, IconExtractor
from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader
from roentgen.scheme import Scheme
from roentgen.direction import DirectionSet
from roentgen.direction import DirectionSet, Sector
ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml"
@ -320,46 +320,82 @@ class Painter:
for node in nodes:
if not(node.get_tag("natural") == "tree" and
"diameter_crown" in node.tags):
("diameter_crown" in node.tags or
"circumference" in node.tags)):
continue
self.svg.add(Circle(
node.point, float(node.tags["diameter_crown"]) * 1.2,
fill="#688C44", stroke="#688C44", opacity=0.3))
if "circumference" in node.tags:
self.svg.add(self.svg.circle(
node.point,
float(node.tags["circumference"]) * self.flinger.scale / 2,
fill="#AAAA88", opacity=0.3))
if "diameter_crown" in node.tags:
self.svg.add(self.svg.circle(
node.point,
float(node.tags["diameter_crown"]) * self.flinger.scale / 2,
fill=self.scheme.get_color("evergreen"), opacity=0.3))
# Directions
for node in nodes: # type: Node
direction = None
if node.get_tag("tourism") == "viewpoint":
direction = node.get_tag("direction")
angle = None
is_revert_gradient: bool = False
if node.get_tag("man_made") == "surveillance":
direction = node.get_tag("camera:direction")
if "camera:angle" in node.tags:
angle = float(node.get_tag("camera:angle"))
if "angle" in node.tags:
angle = float(node.get_tag("angle"))
direction_radius: int = 25 * self.flinger.scale
direction_color: str = \
self.scheme.get_color("direction_camera_color")
elif node.get_tag("traffic_sign") == "stop":
direction = node.get_tag("direction")
direction_radius: int = 25 * self.flinger.scale
direction_color: str = "#FF0000"
else:
direction = node.get_tag("direction")
direction_radius: int = 100 * self.flinger.scale
direction_color: str = \
self.scheme.get_color("direction_view_color")
is_revert_gradient = True
if not direction:
continue
DIRECTION_RADIUS: int = 25
DIRECTION_COLOR: str = self.scheme.get_color("direction_color")
point = (node.point.astype(int)).astype(float)
for path in DirectionSet(direction)\
.draw(node.point, DIRECTION_RADIUS):
if angle:
paths = [Sector(direction, angle)
.draw(point, direction_radius)]
else:
paths = DirectionSet(direction) \
.draw(point, direction_radius)
for path in paths:
gradient = self.svg.defs.add(self.svg.radialGradient(
center=node.point, r=DIRECTION_RADIUS,
center=point, r=direction_radius,
gradientUnits="userSpaceOnUse"))
gradient\
.add_stop_color(0, DIRECTION_COLOR, opacity=0)\
.add_stop_color(1, DIRECTION_COLOR, opacity=0.4)
if is_revert_gradient:
gradient \
.add_stop_color(0, direction_color, opacity=0) \
.add_stop_color(1, direction_color, opacity=0.7)
else:
gradient \
.add_stop_color(0, direction_color, opacity=0.4) \
.add_stop_color(1, direction_color, opacity=0)
self.svg.add(self.svg.path(
d=["M", node.point] + path + ["L", node.point, "Z"],
d=["M", point] + path + ["L", point, "Z"],
fill=gradient.get_paint_server()))
# All other nodes
nodes = sorted(nodes, key=lambda x: x.layer)
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:
if node.get_tag("natural") == "tree" and \
("diameter_crown" in node.tags or
"circumference" in node.tags):
continue
ui.progress_bar(index, len(nodes), step=10)
self.draw_shapes(node, points)
@ -517,7 +553,8 @@ def main():
scheme: Scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME)
flinger: GeoFlinger = GeoFlinger(min1, max1, [0, 0], [w, h])
flinger: GeoFlinger = \
GeoFlinger(min1, max1, np.array([0, 0]), np.array([w, h]))
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)

View file

@ -173,7 +173,7 @@ class Map:
self.node_map[node.id_] = node
if node.user:
self.authors.add(node.user)
self.time.add(node.timestamp)
self.time.update(node.timestamp)
def add_way(self, way: OSMWay):
"""
@ -182,7 +182,7 @@ class Map:
self.way_map[way.id_] = way
if way.user:
self.authors.add(way.user)
self.time.add(way.timestamp)
self.time.update(way.timestamp)
def add_relation(self, relation: OSMRelation):
"""

View file

@ -1,8 +1,20 @@
class MinMax:
"""
Minimum and maximum.
"""
def __init__(self):
self.min_ = None
self.max_ = None
def add(self, value):
def update(self, value):
"""
Update minimum and maximum with new value.
"""
self.min_ = value if not self.min_ or value < self.min_ else self.min_
self.max_ = value if not self.max_ or value > self.max_ else self.max_
def delta(self):
"""
Difference between maximum and minimum.
"""
return self.max_ - self.min_