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: if "addr:housenumber" in tags:
address.append(tags["addr:housenumber"]) address.append(tags["addr:housenumber"])
tags.pop("addr:housenumber", None) tags.pop("addr:housenumber", None)

View file

@ -27,6 +27,8 @@ class Node:
self, icon_set: IconSet, tags: Dict[str, str], self, icon_set: IconSet, tags: Dict[str, str],
point: (float, float), path: Optional[str], point: (float, float), path: Optional[str],
priority: int = 0, is_for_node: bool = True): priority: int = 0, is_for_node: bool = True):
assert point is not None
self.icon_set: IconSet = icon_set self.icon_set: IconSet = icon_set
self.tags = tags self.tags = tags
self.point = point self.point = point
@ -80,8 +82,8 @@ def line_center(nodes: List[OSMNode], flinger: GeoFlinger) -> np.array:
for node in nodes: # type: OSMNode for node in nodes: # type: OSMNode
flung = flinger.fling(node.position) flung = flinger.fling(node.position)
x.add(flung[0]) x.update(flung[0])
y.add(flung[1]) y.update(flung[1])
return np.array(((x.min_ + x.max_) / 2.0, (y.min_ + y.max_) / 2.0)) return np.array(((x.min_ + x.max_) / 2.0, (y.min_ + y.max_) / 2.0))
@ -296,17 +298,19 @@ class Constructor:
style[key] = value style[key] = value
self.ways.append( self.ways.append(
Way(kind, nodes, path, style, layer, 50, levels)) Way(kind, nodes, path, style, layer, 50, levels))
if center_point is not None and way.is_cycle() or \ if center_point is not None and \
"area" in tags and tags["area"]: (way.is_cycle() or "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags) icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node( self.nodes.append(Node(
icon_set, tags, center_point, path, is_for_node=False)) icon_set, tags, center_point, path, is_for_node=False))
appended = True appended = True
if not appended and DEBUG: if not appended:
if DEBUG:
style: Dict[str, Any] = { style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1} "fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way(kind, nodes, path, style, layer, 50, levels)) self.ways.append(Way(
kind, nodes, path, style, layer, 50, levels))
if center_point is not None and way.is_cycle() or \ if center_point is not None and way.is_cycle() or \
"area" in tags and tags["area"]: "area" in tags and tags["area"]:
icon_set: IconSet = self.scheme.get_icon(tags) 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 import numpy as np
from portolan import middle 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 Parse vector from text representation: compass points or 360-degree
notation. E.g. "NW", "270". notation. E.g. "NW", "270".
@ -17,16 +28,19 @@ def parse_vector(text: str) -> np.array:
:param text: vector text representation :param text: vector text representation
:return: parsed normalized vector :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: 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))) return np.array((np.cos(radians), np.sin(radians)))
except ValueError: 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))) return np.array((np.cos(radians), np.sin(radians)))
except KeyError:
pass
return None
def rotation_matrix(angle): def rotation_matrix(angle):
@ -44,25 +58,32 @@ class Sector:
""" """
Sector described by two vectors. 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 text: sector text representation. E.g. "70-210", "N-NW"
:param angle: angle in degrees
""" """
self.start: Optional[np.array] self.start: Optional[np.array] = None
self.end: Optional[np.array] self.end: Optional[np.array] = None
if "-" in text: if "-" in text:
parts: List[str] = text.split("-") parts: List[str] = text.split("-")
self.start = parse_vector(parts[0]) self.start = parse_vector(parts[0])
self.end = parse_vector(parts[1]) self.end = parse_vector(parts[1])
else: else:
vector = parse_vector(text) if angle is None:
angle = np.pi / 12 angle = DEFAULT_ANGLE
else:
angle = degree_to_radian(angle) / 2
angle = max(SMALLEST_ANGLE, angle)
vector: Optional[np.array] = parse_vector(text)
if vector is not None:
self.start = np.dot(rotation_matrix(angle), vector) self.start = np.dot(rotation_matrix(angle), vector)
self.end = np.dot(rotation_matrix(-angle), vector) self.end = np.dot(rotation_matrix(-angle), vector)
def draw(self, center: np.array, radius: float) \ def draw(self, center: np.array, radius: float) -> Optional[List[Path]]:
-> Optional[List[Union[float, str, np.array]]]:
""" """
Construct SVG path commands for arc element. Construct SVG path commands for arc element.
@ -95,7 +116,7 @@ class DirectionSet:
def __str__(self): def __str__(self):
return ", ".join(map(str, self.sectors)) 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. Construct SVG "d" for arc elements.

View file

@ -5,8 +5,6 @@ import math
import numpy as np import numpy as np
from typing import Optional from typing import Optional
from roentgen.util import MinMax
def get_ratio(maximum, minimum, ratio: float = 1): def get_ratio(maximum, minimum, ratio: float = 1):
return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1]) return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1])
@ -48,7 +46,7 @@ class Geo:
class GeoFlinger: class GeoFlinger:
def __init__( 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 minimum: minimum latitude and longitude
:param maximum: maximum latitude and longitude :param maximum: maximum latitude and longitude
@ -71,7 +69,7 @@ class GeoFlinger:
# Ratio is x / y. # Ratio is x / y.
space = [0, 0] space: np.array = [0, 0]
current_ratio = get_ratio(self.maximum, self.minimum, ratio) current_ratio = get_ratio(self.maximum, self.minimum, ratio)
target_ratio = get_ratio(target_maximum, target_minimum) target_ratio = get_ratio(target_maximum, target_minimum)
@ -93,6 +91,13 @@ class GeoFlinger:
self.target_minimum = np.add(target_minimum, space) self.target_minimum = np.add(target_minimum, space)
self.target_maximum = np.subtract(target_maximum, 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 self.space = space
def fling(self, current) -> np.array: def fling(self, current) -> np.array:

View file

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

View file

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

View file

@ -1,8 +1,20 @@
class MinMax: class MinMax:
"""
Minimum and maximum.
"""
def __init__(self): def __init__(self):
self.min_ = None self.min_ = None
self.max_ = 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.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_ 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_