mirror of
https://github.com/enzet/map-machine.git
synced 2025-08-02 08:09:57 +02:00
Fix direction; add scale computing.
This commit is contained in:
parent
197ee5c0ec
commit
8d8080181e
7 changed files with 133 additions and 55 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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_
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue