map-machine/roentgen/mapper.py
2020-08-30 10:41:39 +03:00

591 lines
21 KiB
Python

"""
Simple OpenStreetMap renderer.
Author: Sergey Vartanov (me@enzet.ru).
"""
import numpy as np
import os
import svgwrite
import sys
import yaml
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.shapes import Circle, Rect
from svgwrite.text import Text
from typing import List
from roentgen import extract_icon
from roentgen import ui
from roentgen.constructor import Constructor, get_path
from roentgen.flinger import GeoFlinger, Geo
from roentgen.grid import draw_grid
from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader
ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml"
COLORS_FILE_NAME: str = "data/colors.yml"
MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"
class Painter:
def __init__(
self, show_missing_tags, overlap, draw_nodes, mode, draw_captions,
map_, flinger, svg: svgwrite.Drawing, icons, scheme):
self.show_missing_tags = show_missing_tags
self.overlap = overlap
self.draw_nodes = draw_nodes
self.mode = mode
self.draw_captions = draw_captions
self.map_ = map_
self.flinger = flinger
self.svg: svgwrite.Drawing = svg
self.icons = icons
self.scheme = scheme
def no_draw(self, key):
if key in self.scheme["tags_to_write"] or \
key in self.scheme["tags_to_skip"]:
return True
for prefix in \
self.scheme["prefix_to_write"] + self.scheme["prefix_to_skip"]:
if key[:len(prefix) + 1] == prefix + ":":
return True
return False
def to_write(self, key):
if key in self.scheme["tags_to_skip"]:
return False
if key in self.scheme["tags_to_write"]:
return True
for prefix in self.scheme["prefix_to_write"]:
if key[:len(prefix) + 1] == prefix + ":":
return True
return False
def draw_shapes(self, shapes, points, x, y, fill, tags, processed):
xxx = -(len(shapes) - 1) * 8
if self.overlap != 0:
for shape in shapes:
has_space = True
for p in points[-1000:]:
if x + xxx - self.overlap <= p[0] <= x + xxx + self.overlap and \
y - self.overlap <= p[1] <= y + self.overlap:
has_space = False
break
if has_space:
self.draw_point_shape(shape, x + xxx, y, fill, tags=tags)
points.append([x + xxx, y])
xxx += 16
else:
for shape in shapes:
self.draw_point_shape(shape, x + xxx, y, fill, tags=tags)
xxx += 16
def draw_texts(self, shapes, points, x, y, fill, tags, processed):
if self.draw_captions == "no":
return
text_y: float = 0
write_tags = self.construct_text(tags, processed)
for text_struct in write_tags:
fill = text_struct["fill"] if "fill" in text_struct else "444444"
size = text_struct["size"] if "size" in text_struct else 10
text_y += size + 1
self.wr(text_struct["text"], x, y, fill, text_y, size=size)
if self.show_missing_tags:
for k in tags:
if not self.no_draw(k) and k not in processed:
text = k + ": " + tags[k]
self.draw_text(text, x, float(y) + text_y + 18, "734A08")
text_y += 10
def draw_text(self, text: str, x, y, fill, size=10, out_fill="FFFFFF",
out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0):
"""
Drawing text.
###### ### outline 2
#------# --- outline 1
#| Text |#
#------#
######
"""
if out_fill_2:
self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{out_fill_2}",
stroke_linejoin="round", stroke_width=5,
stroke=f"#{out_fill_2}", opacity=out_opacity_2))
if out_fill:
self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{out_fill}",
stroke_linejoin="round", stroke_width=3,
stroke=f"#{out_fill}", opacity=out_opacity))
self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{fill}"))
def wr(self, text, x, y, fill, text_y, size=10):
text = text[:26] + ("..." if len(text) > 26 else "")
self.draw_text(text, x, float(y) + text_y + 8, fill, size=size)
def construct_text(self, tags, processed):
for key in tags:
tags[key] = tags[key].replace("&quot;", '"')
texts = []
address: List[str] = []
name = None
alt_name = None
if "name" in tags:
name = tags["name"]
tags.pop("name", None)
if "name:ru" in tags:
if not name:
name = tags["name:ru"]
tags.pop("name:ru", None)
tags.pop("name:ru", None)
if "name:en" in tags:
if not name:
name = tags["name:en"]
tags.pop("name:en", None)
tags.pop("name:en", None)
if "alt_name" in tags:
if alt_name:
alt_name += ", "
else:
alt_name = ""
alt_name += tags["alt_name"]
tags.pop("alt_name")
if "old_name" in tags:
if alt_name:
alt_name += ", "
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)
if name:
texts.append({"text": name, "fill": "000000"})
if alt_name:
texts.append({"text": "(" + alt_name + ")"})
if address:
texts.append({"text": ", ".join(address)})
if self.draw_captions == "main":
return texts
if "route_ref" in tags:
texts.append({"text": tags["route_ref"].replace(";", " ")})
tags.pop("route_ref", None)
if "cladr:code" in tags:
texts.append({"text": tags["cladr:code"], "size": 7})
tags.pop("cladr:code", None)
if "website" in tags:
link = tags["website"]
if link[:7] == "http://":
link = link[7:]
if link[:8] == "https://":
link = link[8:]
if link[:4] == "www.":
link = link[4:]
if link[-1] == "/":
link = link[:-1]
link = link[:25] + ("..." if len(tags["website"]) > 25 else "")
texts.append({"text": link, "fill": "000088"})
tags.pop("website", None)
for k in ["phone"]:
if k in tags:
texts.append({"text": tags[k], "fill": "444444"})
tags.pop(k)
for tag in tags:
if self.to_write(tag) and not (tag in processed):
# texts.append({"text": tag + ": " + tags[tag]})
texts.append({"text": tags[tag]})
return texts
def draw_building_walls(self, stage, color, ways):
"""
Draw area between way and way shifted by the vector.
"""
for way in ways:
if way.kind != "building":
continue
if stage == 1:
shift_1 = [0, 0]
shift_2 = [0, -1]
elif stage == 2:
shift_1 = [0, -1]
shift_2 = [0, -2]
else:
shift_1 = [0, -2]
if way.levels:
shift_2 = [0, min(-3, -1 * way.levels)]
else:
shift_2 = [0, -3]
if way.nodes:
for i in range(len(way.nodes) - 1):
node_1 = self.map_.node_map[way.nodes[i]]
node_2 = self.map_.node_map[way.nodes[i + 1]]
flung_1 = self.flinger.fling(Geo(node_1.lat, node_1.lon))
flung_2 = self.flinger.fling(Geo(node_2.lat, node_2.lon))
self.svg.add(self.svg.path(
d=("M", np.add(flung_1, shift_1), "L",
np.add(flung_2, shift_1), np.add(flung_2, shift_2),
np.add(flung_1, shift_2), "Z"),
fill=f"#{color}", stroke=f"#{color}", stroke_width=1))
elif way.path:
# TODO: implement
pass
def draw(self, nodes, ways, points):
ways = sorted(ways, key=lambda x: x.layer)
for way in ways:
if way.kind == "way":
if way.nodes:
path = get_path(way.nodes, [0, 0], self.map_, self.flinger)
self.svg.add(Path(d=path, style=way.style))
else:
self.svg.add(Path(d=way.path, style=way.style))
# Building shade
building_shade = Group(opacity=0.1)
for way in ways:
if way.kind != "building" or not way.nodes:
continue
shift = [-5, 5]
if way.levels:
shift = [-5 * way.levels, 5 * way.levels]
for i in range(len(way.nodes) - 1):
node_1 = self.map_.node_map[way.nodes[i]]
node_2 = self.map_.node_map[way.nodes[i + 1]]
flung_1 = self.flinger.fling(Geo(node_1.lat, node_1.lon))
flung_2 = self.flinger.fling(Geo(node_2.lat, node_2.lon))
building_shade.add(Path(
("M", flung_1, "L", flung_2, np.add(flung_2, shift),
np.add(flung_1, shift), "Z"),
fill="#000000", stroke="#000000", stroke_width=1))
self.svg.add(building_shade)
# Building walls
self.draw_building_walls(1, "AAAAAA", ways)
self.draw_building_walls(2, "C3C3C3", ways)
self.draw_building_walls(3, "DDDDDD", ways)
# Building roof
for way in ways:
if way.kind != "building":
continue
if way.nodes:
shift = [0, -3]
if way.levels:
shift = [0 * way.levels, min(-3, -1 * way.levels)]
path = get_path(way.nodes, shift, self.map_, self.flinger)
self.svg.add(Path(d=path, style=way.style, opacity=1))
else:
self.svg.add(Path(d=way.path, style=way.style, opacity=1))
# Trees
for node in nodes:
if not("natural" in node.tags and
node.tags["natural"] == "tree" and
"diameter_crown" in node.tags):
continue
self.svg.add(Circle(
(float(node.x), float(node.y)),
float(node.tags["diameter_crown"]) * 1.2,
fill="#688C44", stroke="#688C44", opacity=0.3))
# All other nodes
nodes = sorted(nodes, key=lambda x: x.layer)
for node in nodes:
if "natural" in node.tags and \
node.tags["natural"] == "tree" and \
"diameter_crown" in node.tags:
continue
self.draw_shapes(
node.shapes, points, node.x, node.y, node.color, node.tags,
node.processed)
for node in nodes:
if self.mode not in ["time", "user-coloring"]:
self.draw_texts(
node.shapes, points, node.x, node.y, node.color,
node.tags, node.processed)
def draw_point_shape(self, name, x, y, fill, tags=None):
if not isinstance(name, list):
name = [name]
if self.mode not in ["time", "user-coloring"]:
for one_name in name:
shape, xx, yy, _ = self.icons.get_path(one_name)
self.draw_point_outline(
shape, x, y, fill, mode=self.mode, size=16, xx=xx, yy=yy)
for one_name in name:
shape, xx, yy, _ = self.icons.get_path(one_name)
self.draw_point(shape, x, y, fill, size=16, xx=xx, yy=yy, tags=tags)
def draw_point(self, shape, x, y, fill, size=16, xx=0, yy=0, tags=None):
x = int(float(x))
y = int(float(y))
path = self.svg.path(
d=shape, fill=f"#{fill}", fill_opacity=1,
transform=f"translate({x - size / 2.0 - xx * 16},"
f"{y - size / 2.0 - yy * 16})")
path.set_desc(title="\n".join(map(lambda x: x + ": " + tags[x], tags)))
self.svg.add(path)
def draw_point_outline(
self, shape, x, y, fill, mode="default", size=16, xx=0, yy=0):
x = int(float(x))
y = int(float(y))
opacity = 0.5
stroke_width = 2.2
outline_fill = self.scheme["colors"]["outline_color"]
if mode not in ["user-coloring", "time"]:
r = int(fill[0:2], 16)
g = int(fill[2:4], 16)
b = int(fill[4:6], 16)
Y = 0.2126 * r + 0.7152 * g + 0.0722 * b
if Y > 200:
outline_fill = "000000"
opacity = 0.7
self.svg.add(self.svg.path(
d=shape, fill=f"#{outline_fill}", opacity=opacity,
stroke=f"#{outline_fill}", stroke_width=stroke_width,
stroke_linejoin="round",
transform=f"translate({x - size / 2.0 - xx * 16},"
f"{y - size / 2.0 - yy * 16})"))
def check_level_number(tags, level):
if "level" in tags:
levels = \
map(lambda x: float(x), tags["level"].replace(",", ".").split(";"))
if level not in levels:
return False
else:
return False
return True
def check_level_overground(tags):
if "level" in tags:
levels = \
map(lambda x: float(x), tags["level"].replace(",", ".").split(";"))
for level in levels:
if level <= 0:
return False
return True
def main():
if len(sys.argv) == 2:
if sys.argv[1] == "grid":
draw_grid()
return
options = ui.parse_options(sys.argv)
if not options:
sys.exit(1)
background_color = "#EEEEEE"
if options.mode in ["user-coloring", "time"]:
background_color = "#111111"
if options.input_file_name:
input_file_name = options.input_file_name
else:
content = get_osm(options.boundary_box)
if not content:
ui.error("cannot download OSM data")
input_file_name = [os.path.join("map", options.boundary_box + ".osm")]
boundary_box = list(map(
lambda x: float(x.replace('m', '-')), options.boundary_box.split(',')))
full = False # Full keys getting
if options.mode in ["user-coloring", "time"]:
full = True
osm_reader = OSMReader()
for file_name in input_file_name:
if not os.path.isfile(file_name):
print("Fatal: no such file: " + file_name + ".")
sys.exit(1)
osm_reader.parse_osm_file(
file_name, parse_ways=options.draw_ways,
parse_relations=options.draw_ways, full=full)
map_: Map = osm_reader.map_
w, h = list(map(lambda x: float(x), options.size.split(",")))
svg: svgwrite.Drawing = \
svgwrite.Drawing(options.output_file_name, size=(w, h))
svg.add(Rect((0, 0), (w, h), fill=background_color))
min1 = Geo(boundary_box[1], boundary_box[0])
max1 = Geo(boundary_box[3], boundary_box[2])
authors = {}
missing_tags = {}
points = []
scheme = yaml.load(open(TAGS_FILE_NAME), Loader=yaml.FullLoader)
scheme["cache"] = {}
w3c_colors = yaml.load(open(COLORS_FILE_NAME), Loader=yaml.FullLoader)
for color_name in w3c_colors:
scheme["colors"][color_name] = w3c_colors[color_name]
flinger = GeoFlinger(min1, max1, [0, 0], [w, h])
icons = extract_icon.IconExtractor(ICONS_FILE_NAME)
def check_level(x):
""" Draw objects on all levels. """
return True
if options.level:
if options.level == "overground":
check_level = check_level_overground
elif options.level == "underground":
def check_level(x):
""" Draw underground objects. """
return not check_level_overground(x)
else:
def check_level(x):
""" Draw objects on the specified level. """
return not check_level_number(x, float(options.level))
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(
show_missing_tags=options.show_missing_tags, overlap=options.overlap,
draw_nodes=options.draw_nodes, mode=options.mode,
draw_captions=options.draw_captions,
map_=map_, flinger=flinger, svg=svg, icons=icons,
scheme=scheme)
painter.draw(constructor.nodes, constructor.ways, points)
if flinger.space[0] == 0:
svg.add(Rect((0, 0), (w, flinger.space[1]), fill="#FFFFFF"))
svg.add(Rect(
(0, h - flinger.space[1]), (w, flinger.space[1]), fill="#FFFFFF"))
if flinger.space[1] == 0:
svg.add(Rect((0, 0), (flinger.space[0], h), fill="#FFFFFF"))
svg.add(Rect(
(w - flinger.space[0], 0), (flinger.space[0], h), fill="#FFFFFF"))
if options.show_index:
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:
node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step)
j = int((node.lon - min1.lon) / lon_step)
if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += 1
if "tags" in node:
matrix[i][j] += len(node.tags)
for way_id in map_.way_map:
way = map_.way_map[way_id]
if "tags" in way:
for node_id in way.nodes:
node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step)
j = int((node.lon - min1.lon) / lon_step)
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(
min1.lat + i * lat_step, min1.lon + j * lon_step))
t2 = flinger.fling(Geo(
min1.lat + (i + 1) * lat_step,
min1.lon + (j + 1) * lon_step))
svg.add(Text(
str(int(matrix[i][j])),
(((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]}")