map-machine/roentgen/constructor.py
2020-08-26 23:00:41 +03:00

678 lines
23 KiB
Python

import numpy as np
from hashlib import sha256
from datetime import datetime
from typing import Any, Dict, List, Optional, Set
from roentgen import process, ui
from roentgen.flinger import Geo, GeoFlinger
from roentgen.osm_reader import OSMMember, OSMRelation, OSMWay
class Node:
"""
Node in Röntgen terms.
"""
def __init__(
self, shapes, tags: Dict[str, str], x: float, y: float, color: str,
path: Optional[str], processed, priority=0):
self.shapes = shapes
self.tags = tags
self.x = x
self.y = y
self.color = color
self.path = path
self.processed = processed
self.priority = priority
self.layer = 0
class Way:
"""
Way in Röntgen terms.
"""
def __init__(
self, kind: str, nodes, path, style, layer: float = 0.0,
priority: float = 0, levels=None):
self.kind = kind
self.nodes = nodes
self.path = path
self.style = style
self.layer = layer
self.priority = priority
self.levels = levels
def get_float(string):
"""
Try to parse float from a string.
"""
try:
return float(string)
except ValueError:
return 0
def line_center(nodes, flinger: GeoFlinger):
"""
Get geometric center of nodes set.
"""
ma = [0, 0]
mi = [10000, 10000]
for node in nodes:
flung = flinger.fling(Geo(node.lat, node.lon))
if flung[0] > ma[0]:
ma[0] = flung[0]
if flung[1] > ma[1]:
ma[1] = flung[1]
if flung[0] < mi[0]:
mi[0] = flung[0]
if flung[1] < mi[1]:
mi[1] = flung[1]
return [(ma[0] + mi[0]) / 2.0, (ma[1] + mi[1]) / 2.0]
def get_user_color(text: str, seed: str):
"""
Generate random color based on text.
"""
if text == "":
return "000000"
rgb = sha256((seed + text).encode("utf-8")).hexdigest()[-6:]
r = int(rgb[0:2], 16)
g = int(rgb[2:4], 16)
b = int(rgb[4:6], 16)
c = (r + g + b) / 3.
cc = 0
r = r * (1 - cc) + c * cc
g = g * (1 - cc) + c * cc
b = b * (1 - cc) + c * cc
h = hex(int(r))[2:] + hex(int(g))[2:] + hex(int(b))[2:]
return "0" * (6 - len(h)) + h
def get_time_color(time):
"""
Generate color based on time.
"""
if not time:
return "000000"
time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
delta = (datetime.now() - time).total_seconds()
time_color = hex(0xFF - min(0xFF, int(delta / 500000.)))[2:]
i_time_color = hex(min(0xFF, int(delta / 500000.)))[2:]
if len(time_color) == 1:
time_color = "0" + time_color
if len(i_time_color) == 1:
i_time_color = "0" + i_time_color
return time_color + "AA" + i_time_color
def glue(ways: List[OSMWay]):
"""
Try to glue ways that share nodes.
"""
result: List[List[int]] = []
to_process: Set[OSMWay] = set()
for way in ways: # type: OSMWay
if way.is_cycle():
result.append(way.nodes)
else:
to_process.add(way)
while to_process:
way: OSMWay = to_process.pop()
glued: Optional[OSMWay] = None
for other_way in to_process: # type: OSMWay
glued = way.try_to_glue(other_way)
if glued:
break
if glued:
to_process.remove(other_way)
if glued.is_cycle():
result.append(glued.nodes)
else:
to_process.add(glued)
else:
result.append(way.nodes)
return result
def get_path(nodes, shift, map_, flinger: GeoFlinger):
"""
Construct SVG path from nodes.
"""
path = ""
prev_node = None
for node_id in nodes:
node = map_.node_map[node_id]
flung = np.add(flinger.fling(Geo(node.lat, node.lon)), shift)
path += ("L" if prev_node else "M") + f" {flung[0]},{flung[1]} "
prev_node = map_.node_map[node_id]
if nodes[0] == nodes[-1]:
path += "Z"
else:
path = path[:-1]
return path
class Constructor:
"""
Röntgen node and way constructor.
"""
def __init__(self, check_level, mode, seed, map_, flinger, scheme):
self.check_level = check_level
self.mode = mode
self.seed = seed
self.map_ = map_
self.flinger = flinger
self.scheme = scheme
self.nodes: List[Node] = []
self.ways: List[Way] = []
def color(self, name: str):
"""
Get color from the scheme.
"""
return self.scheme["colors"][name]
def construct_ways(self):
"""
Construct Röntgen ways.
"""
for way_id in self.map_.way_map: # type: int
way: OSMWay = self.map_.way_map[way_id]
if not self.check_level(way.tags):
continue
self.construct_way(way, way.tags, None)
def construct_way(
self, way: Optional[OSMWay], tags: Dict[str, Any],
path: Optional[str]) -> None:
"""
Way construction.
:param way: OSM way.
:param tags: way tag dictionary.
:param path: way path (if there is no nodes).
"""
layer: float = 0
level: float = 0
if "layer" in tags:
layer = get_float(tags["layer"])
if "level" in tags:
try:
levels = list(map(lambda x: float(x), tags["level"].split(";")))
level = sum(levels) / len(levels)
except ValueError:
pass
layer = 100 * level + 0.01 * layer
nodes = None
if way:
c = line_center(
map(lambda x: self.map_.node_map[x], way.nodes), self.flinger)
nodes = way.nodes
if self.mode == "user-coloring":
if not way:
return
user_color = get_user_color(way.user, self.seed)
self.ways.append(
Way("way", nodes, path,
f"fill:none;stroke:#{user_color};"
f"stroke-width:1;"))
return
if self.mode == "time":
if not way:
return
time_color = get_time_color(way.timestamp)
self.ways.append(
Way("way", nodes, path,
f"fill:none;stroke:#{time_color};"
f"stroke-width:1;"))
return
# Indoor features
if "indoor" in tags:
v = tags["indoor"]
style = \
f"stroke:#{self.color('indoor_border_color')};" \
f"stroke-width:1;"
if v == "area":
style += f"fill:#{self.color('indoor_color')};"
layer += 10
elif v == "corridor":
style += f"fill:#{self.color('indoor_color')};"
layer += 11
elif v in ["yes", "room", "elevator"]:
style += f"fill:#{self.color('indoor_color')};"
layer += 12
elif v == "column":
style += f"fill:#{self.color('indoor_border_color')};"
layer += 13
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Natural
if "natural" in tags:
v = tags["natural"]
style = "stroke:none;"
if v == "wood":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "grassland":
style = \
f"fill:#{self.color('grass_color')};" \
f"stroke:#{self.color('grass_border_color')};"
layer += 20
elif v == "scrub":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "sand":
style += f"fill:#{self.color('sand_color')};"
layer += 20
elif v == "beach":
style += f"fill:#{self.color('beach_color')};"
layer += 20
elif v == "desert":
style += f"fill:#{self.color('desert_color')};"
layer += 20
elif v == "forest":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "tree_row":
style += \
f"fill:none;stroke:#{self.color('wood_color')};" \
f"stroke-width:5;"
layer += 21
elif v == "water":
style = \
f"fill:#{self.color('water_color')};" \
f"stroke:#{self.color('water_border_color')};" \
f"stroke-width:1.0;"
layer += 21
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Landuse
if "landuse" in tags:
style = "fill:none;stroke:none;"
if tags["landuse"] == "grass":
style = \
f"fill:#{self.color('grass_color')};" \
f"stroke:#{self.color('grass_border_color')};"
layer += 20
elif tags["landuse"] == "conservation":
style = f"fill:#{self.color('grass_color')};stroke:none;"
layer += 20
elif tags["landuse"] == "forest":
style = f"fill:#{self.color('wood_color')};stroke:none;"
layer += 20
elif tags["landuse"] == "garages":
style = f"fill:#{self.color('parking_color')};stroke:none;"
layer += 21
shapes, fill, processed = \
process.get_icon(tags, self.scheme, "444444")
if way:
self.nodes.append(Node(
shapes, tags, c[0], c[1], fill, path, processed))
elif tags["landuse"] == "construction":
layer += 20
style = f"fill:#{self.color('construction_color')};stroke:none;"
elif tags["landuse"] in ["residential", "commercial"]:
return
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Building
if "building" in tags:
layer += 40
levels = 1
if "building:levels" in tags:
levels = float(tags["building:levels"])
style = \
f"fill:#{self.color('building_color')};" \
f"stroke:#{self.color('building_border_color')};" \
f"opacity:1.0;"
shapes, fill, processed = \
process.get_icon(tags, self.scheme, "444444")
if "height" in tags:
try:
layer += float(tags["height"])
except ValueError:
pass
if way:
self.nodes.append(
Node(shapes, tags, c[0], c[1], fill, path, processed, 1))
self.ways.append(Way(
"building", nodes, path, style, layer, 50, levels))
# Amenity
if "amenity" in tags:
style = "fill:none;stroke:none;"
layer += 21
if tags["amenity"] == "parking":
style = \
f"fill:#{self.color('parking_color')};" \
f"stroke:none;opacity:0.5;"
shapes, fill, processed = \
process.get_icon(tags, self.scheme, "444444")
if way:
self.nodes.append(Node(
shapes, tags, c[0], c[1], fill, path, processed, 1))
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Waterway
if "waterway" in tags:
style = "fill:none;stroke:none;"
layer += 21
if tags["waterway"] == "riverbank":
style = \
f"fill:#{self.color('water_color')};" \
f"stroke:#{self.color('water_border_color')};" \
f"stroke-width:1.0;"
elif tags["waterway"] == "river":
style = \
f"fill:none;stroke:#{self.color('water_color')};" \
f"stroke-width:10.0;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Railway
if "railway" in tags:
layer += 41
v = tags["railway"]
style = \
"fill:none;stroke-dasharray:none;stroke-linejoin:round;" \
"stroke-linecap:round;stroke-width:"
if v == "subway":
style += "10;stroke:#DDDDDD;"
if v in ["narrow_gauge", "tram"]:
style += "2;stroke:#000000;"
if v == "platform":
style = \
f"fill:#{self.color('platform_color')};" \
f"stroke:#{self.color('platform_border_color')};" \
f"stroke-width:1;"
else:
return
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Highway
if "highway" in tags:
layer += 42
v = tags["highway"]
style = \
f"fill:none;stroke:#{self.color('road_border_color')};" \
f"stroke-dasharray:none;stroke-linejoin:round;" \
f"stroke-linecap:round;stroke-width:"
# Highway outline
if v == "motorway":
style += "33"
elif v == "trunk":
style += "31"
elif v == "primary":
style += f"29;stroke:#{self.color('primary_border_color')};"
elif v == "secondary":
style += "27"
elif v == "tertiary":
style += "25"
elif v == "unclassified":
style += "17"
elif v == "residential":
style += "17"
elif v == "service":
if "service" in tags and tags["service"] == "parking_aisle":
style += "7"
else:
style += "11"
elif v == "track":
style += "3"
elif v in ["footway", "pedestrian", "cycleway"]:
if not ("area" in tags and tags["area"] == "yes"):
style += f"3;stroke:#{self.color('foot_border_color')};"
elif v in ["steps"]:
style += \
f"6;stroke:#{self.color('foot_border_color')};" \
f"stroke-linecap:butt;"
else:
style = None
if style:
style += ";"
self.ways.append(Way(
"way", nodes, path, style, layer + 41, 50))
# Highway main shape
style = "fill:none;stroke:#FFFFFF;stroke-linecap:round;" + \
"stroke-linejoin:round;stroke-width:"
if v == "motorway":
style += "31"
elif v == "trunk":
style += "29"
elif v == "primary":
style += "27;stroke:#" + self.color('primary_color')
elif v == "secondary":
style += "25"
elif v == "tertiary":
style += "23"
elif v == "unclassified":
style += "15"
elif v == "residential":
style += "15"
elif v == "service":
if "service" in tags and tags["service"] == "parking_aisle":
style += "5"
else:
style += "9"
elif v == "cycleway":
style += \
f"1;stroke-dasharray:8,2;istroke-linecap:butt;" \
f"stroke:#{self.color('cycle_color')}"
elif v in ["footway", "pedestrian"]:
if "area" in tags and tags["area"] == "yes":
style += "1;stroke:none;fill:#DDDDDD"
layer -= 55 # FIXME!
else:
style += \
"1.5;stroke-dasharray:7,3;stroke-linecap:round;stroke:#"
if "guide_strips" in tags and tags["guide_strips"] == "yes":
style += self.color('guide_strips_color')
else:
style += self.color('foot_color')
elif v == "steps":
style += "5;stroke-dasharray:1.5,2;stroke-linecap:butt;" + \
"stroke:#"
if "conveying" in tags:
style += "888888"
else:
style += self.color('foot_color')
elif v == "path":
style += "1;stroke-dasharray:5,5;stroke-linecap:butt;" + \
"stroke:#" + self.color('foot_color')
style += ";"
self.ways.append(Way("way", nodes, path, style, layer + 42, 50))
if "oneway" in tags and tags["oneway"] == "yes" or \
"conveying" in tags and tags["conveying"] == "forward":
for k in range(7):
self.ways.append(Way(
"way", nodes, path,
f"fill:none;stroke:#EEEEEE;stroke-linecap:butt;"
f"stroke-width:{7 - k};stroke-dasharray:{k},{40 - k};",
layer + 43, 50))
if "access" in tags and tags["access"] == "private":
self.ways.append(Way(
"way", nodes, path,
f"fill:none;stroke:#{self.color('private_access_color')};"
f"stroke-linecap:butt;stroke-width:10;stroke-dasharray:1,5;"
f"opacity:0.4;", layer + 0.1, 50))
# Leisure
if "leisure" in tags:
layer += 21
if tags["leisure"] == "playground":
style = f"fill:#{self.color('playground_color')};opacity:0.2;"
# FIXME!!!!!!!!!!!!!!!!!!!!!
# if nodes:
# self.draw_point_shape("toy_horse", c[0], c[1], "444444")
elif tags["leisure"] == "garden":
style = f"fill:#{self.color('grass_color')};"
elif tags["leisure"] == "pitch":
style = f"fill:#{self.color('playground_color')};opacity:0.2;"
elif tags["leisure"] == "park":
return
else:
style = "fill:#FF0000;opacity:0.2;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Barrier
if "barrier" in tags:
style = "fill:none;stroke:none;"
layer += 40
if tags["barrier"] == "hedge":
style += \
f"fill:none;stroke:#{self.color('wood_color')};" \
f"stroke-width:4;"
elif tags["barrier"] == "fense":
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.4;"
elif tags["barrier"] == "kerb":
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.2;"
else:
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.3;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Border
if "border" in tags:
style = "fill:none;stroke:none;"
style += "fill:none;stroke:#FF0000;stroke-width:0.5;" + \
"stroke-dahsarray:10,20;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
if "area:highway" in tags:
style = "fill:none;stroke:none;"
if tags["area:highway"] == "yes":
style += "fill:#FFFFFF;stroke:#DDDDDD;stroke-width:1;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
def construct_relations(self):
"""
Construct Röntgen ways from OSM relations.
"""
for relation_id in self.map_.relation_map:
relation: OSMRelation = self.map_.relation_map[relation_id]
tags = relation.tags
if not self.check_level(tags):
continue
if "type" in tags and tags["type"] == "multipolygon":
inners, outers = [], []
for member in relation.members: # type: OSMMember
if member.type_ == "way":
if member.role == "inner":
if member.ref in self.map_.way_map:
inners.append(self.map_.way_map[member.ref])
elif member.role == "outer":
if member.ref in self.map_.way_map:
outers.append(self.map_.way_map[member.ref])
p = ""
inners_path = glue(inners)
outers_path = glue(outers)
for nodes in outers_path:
path = get_path(nodes, [0, 0], self.map_, self.flinger)
p += path + " "
for nodes in inners_path:
nodes.reverse()
path = get_path(nodes, [0, 0], self.map_, self.flinger)
p += path + " "
self.construct_way(None, tags, p)
def construct_nodes(self):
"""
Draw nodes.
"""
print("Draw nodes...")
start_time = datetime.now()
node_number = 0
# processed_tags = 0
# skipped_tags = 0
s = sorted(
self.map_.node_map.keys(), key=lambda x: -self.map_.node_map[x].lat)
for node_id in s:
node_number += 1
ui.write_line(node_number, len(self.map_.node_map))
node = self.map_.node_map[node_id]
flung = self.flinger.fling(Geo(node.lat, node.lon))
x = flung[0]
y = flung[1]
tags = node.tags
if not self.check_level(tags):
continue
shapes, fill, processed = process.get_icon(tags, self.scheme)
if self.mode in ["time", "user-coloring"]:
if not tags:
continue
shapes = ["small"]
if self.mode == "user-coloring":
fill = get_user_color(node.user, self.seed)
if self.mode == "time":
fill = get_time_color(node.timestamp)
# for k in tags:
# if k in processed or self.no_draw(k):
# processed_tags += 1
# else:
# skipped_tags += 1
# for k in []: # tags:
# if to_write(k):
# draw_text(k + ": " + tags[k], x, y + 18 + text_y,
# "444444")
# text_y += 10
# if show_missing_tags:
# for k in tags:
# v = tags[k]
# if not no_draw(k) and not k in processed:
# if ("node " + k + ": " + v) in missing_tags:
# missing_tags["node " + k + ": " + v] += 1
# else:
# missing_tags["node " + k + ": " + v] = 1
if shapes == [] and tags != {}:
shapes = [["no"]]
self.nodes.append(Node(
shapes, tags, x, y, fill, None, processed))
ui.write_line(-1, len(self.map_.node_map))
print("Nodes painted in " + str(datetime.now() - start_time) + ".")
# print("Tags processed: " + str(processed_tags) + ", tags skipped: " +
# str(skipped_tags) + " (" +
# str(processed_tags / float(
# processed_tags + skipped_tags) * 100) + " %).")