mirror of
https://github.com/enzet/map-machine.git
synced 2025-08-06 10:09:52 +02:00
Issue #20: support viewpoint direction.
This commit is contained in:
parent
be90f4bb09
commit
0405e552e2
5 changed files with 140 additions and 35 deletions
|
@ -16,6 +16,8 @@ colors:
|
||||||
wood: "B8CC84"
|
wood: "B8CC84"
|
||||||
tree: "98AC64"
|
tree: "98AC64"
|
||||||
|
|
||||||
|
direction_color: "E0F0FF"
|
||||||
|
|
||||||
outline_color: "FFFFFF"
|
outline_color: "FFFFFF"
|
||||||
beach_color: "F0E0C0"
|
beach_color: "F0E0C0"
|
||||||
boundary_color: "880088"
|
boundary_color: "880088"
|
||||||
|
|
|
@ -177,12 +177,6 @@ class Constructor:
|
||||||
self.nodes: List[Node] = []
|
self.nodes: List[Node] = []
|
||||||
self.ways: List[Way] = []
|
self.ways: List[Way] = []
|
||||||
|
|
||||||
def color(self, name: str):
|
|
||||||
"""
|
|
||||||
Get color from the scheme.
|
|
||||||
"""
|
|
||||||
return self.scheme.get_color(name)
|
|
||||||
|
|
||||||
def construct_ways(self):
|
def construct_ways(self):
|
||||||
"""
|
"""
|
||||||
Construct Röntgen ways.
|
Construct Röntgen ways.
|
||||||
|
@ -283,7 +277,7 @@ class Constructor:
|
||||||
if key not in ["tags", "no_tags", "layer", "level", "icon"]:
|
if key not in ["tags", "no_tags", "layer", "level", "icon"]:
|
||||||
value = element[key]
|
value = element[key]
|
||||||
if isinstance(value, str) and value.endswith("_color"):
|
if isinstance(value, str) and value.endswith("_color"):
|
||||||
value = "#" + self.scheme.get_color(value)
|
value = self.scheme.get_color(value)
|
||||||
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))
|
||||||
|
@ -294,6 +288,7 @@ class Constructor:
|
||||||
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:
|
if not appended:
|
||||||
style: Dict[str, Any] = {
|
style: Dict[str, Any] = {
|
||||||
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
|
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
|
||||||
|
@ -303,6 +298,7 @@ class Constructor:
|
||||||
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))
|
||||||
|
"""
|
||||||
|
|
||||||
def construct_relations(self) -> None:
|
def construct_relations(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
83
roentgen/direction.py
Normal file
83
roentgen/direction.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"""
|
||||||
|
Direction tag support.
|
||||||
|
|
||||||
|
Author: Sergey Vartanov (me@enzet.ru).
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
:param text: vector text representation
|
||||||
|
:return: parsed normalized vector
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class Sector:
|
||||||
|
def __init__(self, text: str):
|
||||||
|
self.start: Optional[np.array]
|
||||||
|
self.end: Optional[np.array]
|
||||||
|
|
||||||
|
if "-" in text:
|
||||||
|
parts: List[str] = text.split("-")
|
||||||
|
self.start = parse_vector(parts[0])
|
||||||
|
self.end = parse_vector(parts[1])
|
||||||
|
else:
|
||||||
|
self.start = parse_vector(text)
|
||||||
|
self.end = None
|
||||||
|
|
||||||
|
def draw(self, center: np.array, radius: float) -> List:
|
||||||
|
"""
|
||||||
|
Construct SVG "d" for arc element.
|
||||||
|
|
||||||
|
:param center: arc center
|
||||||
|
:param radius: arc radius
|
||||||
|
"""
|
||||||
|
start: np.array = center + radius * self.end
|
||||||
|
end: np.array = center + radius * self.start
|
||||||
|
|
||||||
|
return ["L", start, "A", radius, radius, 0, "0", 0, end]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.start}-{self.end}"
|
||||||
|
|
||||||
|
|
||||||
|
class DirectionSet:
|
||||||
|
"""
|
||||||
|
Describes direction, set of directions.
|
||||||
|
"""
|
||||||
|
def __init__(self, text: str):
|
||||||
|
"""
|
||||||
|
:param text: direction tag value
|
||||||
|
"""
|
||||||
|
self.sectors = list(map(Sector, text.split(";")))
|
||||||
|
|
||||||
|
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)
|
|
@ -22,6 +22,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
|
||||||
|
|
||||||
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"
|
||||||
|
@ -95,7 +96,7 @@ class Painter:
|
||||||
write_tags = self.construct_text(node.tags, node.icon_set.processed)
|
write_tags = self.construct_text(node.tags, node.icon_set.processed)
|
||||||
|
|
||||||
for text_struct in write_tags:
|
for text_struct in write_tags:
|
||||||
fill = text_struct["fill"] if "fill" in text_struct else "444444"
|
fill = text_struct["fill"] if "fill" in text_struct else "#444444"
|
||||||
size = text_struct["size"] if "size" in text_struct else 10
|
size = text_struct["size"] if "size" in text_struct else 10
|
||||||
text_y += size + 1
|
text_y += size + 1
|
||||||
text = text_struct["text"]
|
text = text_struct["text"]
|
||||||
|
@ -113,11 +114,11 @@ class Painter:
|
||||||
text = f"{tag}: {node.tags[tag]}"
|
text = f"{tag}: {node.tags[tag]}"
|
||||||
self.draw_text(
|
self.draw_text(
|
||||||
text, (node.point[0], node.point[1] + text_y + 18),
|
text, (node.point[0], node.point[1] + text_y + 18),
|
||||||
"734A08")
|
"#734A08")
|
||||||
text_y += 10
|
text_y += 10
|
||||||
|
|
||||||
def draw_text(
|
def draw_text(
|
||||||
self, text: str, point, fill, size=10, out_fill="FFFFFF",
|
self, text: str, point, fill, size=10, out_fill="#FFFFFF",
|
||||||
out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0):
|
out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0):
|
||||||
"""
|
"""
|
||||||
Drawing text.
|
Drawing text.
|
||||||
|
@ -131,18 +132,18 @@ class Painter:
|
||||||
if out_fill_2:
|
if out_fill_2:
|
||||||
self.svg.add(Text(
|
self.svg.add(Text(
|
||||||
text, point, font_size=size, text_anchor="middle",
|
text, point, font_size=size, text_anchor="middle",
|
||||||
font_family="Roboto", fill=f"#{out_fill_2}",
|
font_family="Roboto", fill=out_fill_2,
|
||||||
stroke_linejoin="round", stroke_width=5,
|
stroke_linejoin="round", stroke_width=5,
|
||||||
stroke=f"#{out_fill_2}", opacity=out_opacity_2))
|
stroke=out_fill_2, opacity=out_opacity_2))
|
||||||
if out_fill:
|
if out_fill:
|
||||||
self.svg.add(Text(
|
self.svg.add(Text(
|
||||||
text, point, font_size=size, text_anchor="middle",
|
text, point, font_size=size, text_anchor="middle",
|
||||||
font_family="Roboto", fill=f"#{out_fill}",
|
font_family="Roboto", fill=out_fill,
|
||||||
stroke_linejoin="round", stroke_width=3,
|
stroke_linejoin="round", stroke_width=3,
|
||||||
stroke=f"#{out_fill}", opacity=out_opacity))
|
stroke=out_fill, opacity=out_opacity))
|
||||||
self.svg.add(Text(
|
self.svg.add(Text(
|
||||||
text, point, font_size=size, text_anchor="middle",
|
text, point, font_size=size, text_anchor="middle",
|
||||||
font_family="Roboto", fill=f"#{fill}"))
|
font_family="Roboto", fill=fill))
|
||||||
|
|
||||||
def construct_text(self, tags, processed):
|
def construct_text(self, tags, processed):
|
||||||
"""
|
"""
|
||||||
|
@ -197,7 +198,7 @@ class Painter:
|
||||||
address.append(tags["addr:housenumber"])
|
address.append(tags["addr:housenumber"])
|
||||||
tags.pop("addr:housenumber", None)
|
tags.pop("addr:housenumber", None)
|
||||||
if name:
|
if name:
|
||||||
texts.append({"text": name, "fill": "000000"})
|
texts.append({"text": name, "fill": "#000000"})
|
||||||
if alt_name:
|
if alt_name:
|
||||||
texts.append({"text": "(" + alt_name + ")"})
|
texts.append({"text": "(" + alt_name + ")"})
|
||||||
if address:
|
if address:
|
||||||
|
@ -223,11 +224,11 @@ class Painter:
|
||||||
if link[-1] == "/":
|
if link[-1] == "/":
|
||||||
link = link[:-1]
|
link = link[:-1]
|
||||||
link = link[:25] + ("..." if len(tags["website"]) > 25 else "")
|
link = link[:25] + ("..." if len(tags["website"]) > 25 else "")
|
||||||
texts.append({"text": link, "fill": "000088"})
|
texts.append({"text": link, "fill": "#000088"})
|
||||||
tags.pop("website", None)
|
tags.pop("website", None)
|
||||||
for k in ["phone"]:
|
for k in ["phone"]:
|
||||||
if k in tags:
|
if k in tags:
|
||||||
texts.append({"text": tags[k], "fill": "444444"})
|
texts.append({"text": tags[k], "fill": "#444444"})
|
||||||
tags.pop(k)
|
tags.pop(k)
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if self.scheme.is_writable(tag) and not (tag in processed):
|
if self.scheme.is_writable(tag) and not (tag in processed):
|
||||||
|
@ -266,7 +267,7 @@ class Painter:
|
||||||
d=("M", np.add(flung_1, shift_1), "L",
|
d=("M", np.add(flung_1, shift_1), "L",
|
||||||
np.add(flung_2, shift_1), np.add(flung_2, shift_2),
|
np.add(flung_2, shift_1), np.add(flung_2, shift_2),
|
||||||
np.add(flung_1, shift_2), "Z"),
|
np.add(flung_1, shift_2), "Z"),
|
||||||
fill=f"#{color}", stroke=f"#{color}", stroke_width=1))
|
fill=color, stroke=color, stroke_width=1))
|
||||||
elif way.path:
|
elif way.path:
|
||||||
# TODO: implement
|
# TODO: implement
|
||||||
pass
|
pass
|
||||||
|
@ -312,9 +313,9 @@ class Painter:
|
||||||
|
|
||||||
# Building walls
|
# Building walls
|
||||||
|
|
||||||
self.draw_building_walls(1, "AAAAAA", ways)
|
self.draw_building_walls(1, "#AAAAAA", ways)
|
||||||
self.draw_building_walls(2, "C3C3C3", ways)
|
self.draw_building_walls(2, "#C3C3C3", ways)
|
||||||
self.draw_building_walls(3, "DDDDDD", ways)
|
self.draw_building_walls(3, "#DDDDDD", ways)
|
||||||
|
|
||||||
# Building roof
|
# Building roof
|
||||||
|
|
||||||
|
@ -345,6 +346,29 @@ class Painter:
|
||||||
node.point, float(node.tags["diameter_crown"]) * 1.2,
|
node.point, float(node.tags["diameter_crown"]) * 1.2,
|
||||||
fill="#688C44", stroke="#688C44", opacity=0.3))
|
fill="#688C44", stroke="#688C44", opacity=0.3))
|
||||||
|
|
||||||
|
# Directions
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
if not ("tourism" in node.tags and
|
||||||
|
node.tags["tourism"] == "viewpoint" and
|
||||||
|
"direction" in node.tags):
|
||||||
|
continue
|
||||||
|
|
||||||
|
DIRECTION_RADIUS: int = 50
|
||||||
|
DIRECTION_COLOR: str = self.scheme.get_color("direction_color")
|
||||||
|
|
||||||
|
for d in DirectionSet(node.tags["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)
|
||||||
|
self.svg.add(self.svg.path(
|
||||||
|
d=["M", node.point] + d + ["L", node.point, "Z"],
|
||||||
|
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)
|
||||||
|
@ -382,12 +406,12 @@ class Painter:
|
||||||
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
|
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
|
||||||
|
|
||||||
path = icon.get_path(self.svg, point)
|
path = icon.get_path(self.svg, point)
|
||||||
path.update({"fill": f"#{fill}"})
|
path.update({"fill": fill})
|
||||||
path.set_desc(title=title)
|
path.set_desc(title=title)
|
||||||
self.svg.add(path)
|
self.svg.add(path)
|
||||||
|
|
||||||
def draw_point_outline(
|
def draw_point_outline(
|
||||||
self, icon: Icon, point, fill, mode="default", size=16):
|
self, icon: Icon, point, fill, mode="default"):
|
||||||
|
|
||||||
point = np.array(list(map(lambda x: int(x), point)))
|
point = np.array(list(map(lambda x: int(x), point)))
|
||||||
|
|
||||||
|
@ -395,18 +419,18 @@ class Painter:
|
||||||
stroke_width = 2.2
|
stroke_width = 2.2
|
||||||
outline_fill = self.scheme.get_color("outline_color")
|
outline_fill = self.scheme.get_color("outline_color")
|
||||||
if mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
|
if mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
|
||||||
r = int(fill[0:2], 16)
|
r = int(fill[1:3], 16)
|
||||||
g = int(fill[2:4], 16)
|
g = int(fill[3:5], 16)
|
||||||
b = int(fill[4:6], 16)
|
b = int(fill[5:7], 16)
|
||||||
Y = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
Y = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
if Y > 200:
|
if Y > 200:
|
||||||
outline_fill = "000000"
|
outline_fill = "#000000"
|
||||||
opacity = 0.7
|
opacity = 0.7
|
||||||
|
|
||||||
path = icon.get_path(self.svg, point)
|
path = icon.get_path(self.svg, point)
|
||||||
path.update({
|
path.update({
|
||||||
"fill": f"#{outline_fill}", "opacity": opacity,
|
"fill": outline_fill, "opacity": opacity,
|
||||||
"stroke": f"#{outline_fill}", "stroke-width": stroke_width,
|
"stroke": outline_fill, "stroke-width": stroke_width,
|
||||||
"stroke-linejoin": "round"})
|
"stroke-linejoin": "round"})
|
||||||
self.svg.add(path)
|
self.svg.add(path)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Set
|
||||||
|
|
||||||
from roentgen.extract_icon import DEFAULT_SHAPE_ID
|
from roentgen.extract_icon import DEFAULT_SHAPE_ID
|
||||||
|
|
||||||
DEFAULT_COLOR: str = "444444"
|
DEFAULT_COLOR: str = "#444444"
|
||||||
|
|
||||||
|
|
||||||
class IconSet:
|
class IconSet:
|
||||||
|
@ -68,12 +68,12 @@ class Scheme:
|
||||||
"""
|
"""
|
||||||
Return color if the color is in scheme, otherwise return default color.
|
Return color if the color is in scheme, otherwise return default color.
|
||||||
|
|
||||||
:return: 6-digit color specification without "#"
|
:return: 6-digit color specification with "#"
|
||||||
"""
|
"""
|
||||||
if color in self.colors:
|
if color in self.colors:
|
||||||
return self.colors[color]
|
return "#" + self.colors[color]
|
||||||
if color.startswith("#"):
|
if color.startswith("#"):
|
||||||
return color[1:]
|
return color
|
||||||
|
|
||||||
print(f"No color {color}.")
|
print(f"No color {color}.")
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ class Scheme:
|
||||||
for key in matcher["tags"].keys():
|
for key in matcher["tags"].keys():
|
||||||
processed.add(key)
|
processed.add(key)
|
||||||
if "color" in matcher:
|
if "color" in matcher:
|
||||||
fill = self.colors[matcher["color"]]
|
fill = self.get_color(matcher["color"])
|
||||||
for key in matcher["tags"].keys():
|
for key in matcher["tags"].keys():
|
||||||
processed.add(key)
|
processed.add(key)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue