mirror of
https://github.com/enzet/map-machine.git
synced 2025-04-30 18:57:49 +02:00
591 lines
21 KiB
Python
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(""", '"')
|
|
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]}")
|